From ff00edf35d8159b0cc5af3cdd24280662230df98 Mon Sep 17 00:00:00 2001 From: Jonas Linter Date: Sat, 27 Sep 2025 17:35:05 +0200 Subject: [PATCH] Started with db development --- config/config.yaml | 12 +++- output.xml | 7 +- pyproject.toml | 1 + src/alpine_bits_python/__main__.py | 1 + src/alpine_bits_python/config_loader.py | 91 +++++++++++++++++++++++-- src/alpine_bits_python/db.py | 23 ++++--- src/alpine_bits_python/main.py | 26 +++++-- uv.lock | 2 + 8 files changed, 136 insertions(+), 27 deletions(-) diff --git a/config/config.yaml b/config/config.yaml index e1a83c9..063276e 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -5,6 +5,12 @@ database: url: "sqlite:///alpinebits.db" # For local dev, use SQLite. For prod, override with PostgreSQL URL. # url: "postgresql://user:password@host:port/dbname" # Example for Postgres -secrets: - # Example: API keys, tokens, etc. Use annotatedyaml to mark these as secrets. - # api_key: !secret "my-secret-key" +alpine_bits_auth: + - hotel_id: "123" + hotel_name: "Frangart Inn" + username: "alice" + password: !secret ALICE_PASSWORD + - hotel_id: "456" + hotel_name: "Bemelmans" + username: "bob" + password: !secret BOB_PASSWORD \ No newline at end of file diff --git a/output.xml b/output.xml index 8bc6ad3..4c6400f 100644 --- a/output.xml +++ b/output.xml @@ -1,7 +1,7 @@ - + @@ -36,12 +36,9 @@ Landing page comment Wix form submission - - utm_Source: ig | utm_Medium: Instagram_Stories | utm_Campaign: Conversions_Hotel_Bemelmans_ITA | utm_Term: Cold_Traffic_Conversions_Hotel_Bemelmans_ITA | utm_Content: Grafik_4_Spätsommer_23.08-07.09_Landingpage_ITA - - + diff --git a/pyproject.toml b/pyproject.toml index 9c152ba..7c2fd0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ dependencies = [ "slowapi>=0.1.9", "sqlalchemy>=2.0.43", "uvicorn>=0.37.0", + "voluptuous>=0.15.2", "xsdata-pydantic[cli,lxml,soap]>=24.5", "xsdata[cli,lxml,soap]>=25.7", ] diff --git a/src/alpine_bits_python/__main__.py b/src/alpine_bits_python/__main__.py index a3bb46a..9a948c0 100644 --- a/src/alpine_bits_python/__main__.py +++ b/src/alpine_bits_python/__main__.py @@ -2,4 +2,5 @@ from .main import main if __name__ == "__main__": + print("running test main") main() \ No newline at end of file diff --git a/src/alpine_bits_python/config_loader.py b/src/alpine_bits_python/config_loader.py index 90d4e9c..59fcaeb 100644 --- a/src/alpine_bits_python/config_loader.py +++ b/src/alpine_bits_python/config_loader.py @@ -1,10 +1,89 @@ + import os -import yaml -import annotatedyaml +from pathlib import Path +from typing import Any, Dict, List, Optional +from annotatedyaml.loader import ( + HAS_C_LOADER, + JSON_TYPE, + LoaderType, + Secrets, + add_constructor, + load_yaml as load_annotated_yaml, + load_yaml_dict as load_annotated_yaml_dict, + parse_yaml as parse_annotated_yaml, + secret_yaml as annotated_secret_yaml, +) +from voluptuous import Schema, Required, All, Length, PREVENT_EXTRA, MultipleInvalid -CONFIG_PATH = os.path.join(os.path.dirname(__file__), '../../config/config.yaml') +# --- Voluptuous schemas --- +database_schema = Schema({ + Required('url'): str +}, extra=PREVENT_EXTRA) + + +hotel_auth_schema = Schema({ + Required("hotel_id"): str, + Required("hotel_name"): str, + Required("username"): str, + Required("password"): str +}, extra=PREVENT_EXTRA) + +basic_auth_schema = Schema( + All([hotel_auth_schema], Length(min=1)) +) + +config_schema = Schema({ + Required('database'): database_schema, + Required('alpine_bits_auth'): basic_auth_schema +}, extra=PREVENT_EXTRA) + +DEFAULT_CONFIG_FILE = 'config.yaml' + + +class Config: + def __init__(self, config_folder: str | Path = None, config_name: str = DEFAULT_CONFIG_FILE, testing_mode: bool = False): + if config_folder is None: + config_folder = os.environ.get('ALPINEBITS_CONFIG_DIR') + if not config_folder: + config_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../config')) + if isinstance(config_folder, str): + config_folder = Path(config_folder) + self.config_folder = config_folder + self.config_path = os.path.join(config_folder, config_name) + self.secrets = Secrets(config_folder) + self.testing_mode = testing_mode + self._load_config() + + def _load_config(self): + stuff = load_annotated_yaml(self.config_path, secrets=self.secrets) + try: + validated = config_schema(stuff) + except MultipleInvalid as e: + raise ValueError(f"Config validation error: {e}") + self.database = validated['database'] + self.basic_auth = validated['alpine_bits_auth'] + self.config = validated + + def get(self, key, default=None): + return self.config.get(key, default) + + @property + def db_url(self) -> str: + return self.database['url'] + + @property + def hotel_id(self) -> str: + return self.basic_auth['hotel_id'] + + @property + def hotel_name(self) -> str: + return self.basic_auth['hotel_name'] + + @property + def users(self) -> List[Dict[str, str]]: + return self.basic_auth['users'] + +# For backward compatibility def load_config(): - with open(CONFIG_PATH, 'r', encoding='utf-8') as f: - # Use annotatedyaml to load secrets if present - return annotatedyaml.load(f) + return Config().config diff --git a/src/alpine_bits_python/db.py b/src/alpine_bits_python/db.py index dd41835..40bf843 100644 --- a/src/alpine_bits_python/db.py +++ b/src/alpine_bits_python/db.py @@ -46,15 +46,20 @@ class HashedCustomer(Base): redacted_at = Column(DateTime) -def get_engine(): - db_url = os.environ.get('DATABASE_URL') - if db_url: - return create_engine(db_url) - # Default to local sqlite - return create_engine('sqlite:///alpinebits.db') +def get_engine(config=None): + db_url = None + if config and 'database' in config and 'url' in config['database']: + db_url = config['database']['url'] + if not db_url: + db_url = os.environ.get('DATABASE_URL') + if not db_url: + db_url = 'sqlite:///alpinebits.db' + return create_engine(db_url) -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=get_engine()) +def get_session_local(config=None): + engine = get_engine(config) + return sessionmaker(autocommit=False, autoflush=False, bind=engine) -def init_db(): - engine = get_engine() +def init_db(config=None): + engine = get_engine(config) Base.metadata.create_all(bind=engine) diff --git a/src/alpine_bits_python/main.py b/src/alpine_bits_python/main.py index d7db8aa..2555b85 100644 --- a/src/alpine_bits_python/main.py +++ b/src/alpine_bits_python/main.py @@ -18,20 +18,38 @@ from .simplified_access import ( ) # DB and config -from .db import Customer as DBCustomer, Reservation as DBReservation, HashedCustomer, SessionLocal, init_db +from .db import Customer as DBCustomer, Reservation as DBReservation, HashedCustomer, get_session_local, init_db from .config_loader import load_config import hashlib +import json +import os def main(): - import json - import os + print("🚀 Starting AlpineBits XML generation script...") # Load config (yaml, annotatedyaml) config = load_config() + # print config for debugging + print("Loaded configuration:") + print(json.dumps(config, indent=2)) + + # Ensure SQLite DB file exists if using SQLite + db_url = config.get('database', {}).get('url', '') + if db_url.startswith('sqlite:///'): + db_path = db_url.replace('sqlite:///', '') + db_path = os.path.abspath(db_path) + db_dir = os.path.dirname(db_path) + if not os.path.exists(db_dir): + os.makedirs(db_dir, exist_ok=True) + # The DB file will be created by SQLAlchemy if it doesn't exist, but ensure directory exists + # Init DB - init_db() + init_db(config) + + print("📦 Database initialized/ready.") + SessionLocal = get_session_local(config) db = SessionLocal() # Load data from JSON file diff --git a/uv.lock b/uv.lock index fa235c7..076d62f 100644 --- a/uv.lock +++ b/uv.lock @@ -18,6 +18,7 @@ dependencies = [ { name = "slowapi" }, { name = "sqlalchemy" }, { name = "uvicorn" }, + { name = "voluptuous" }, { name = "xsdata", extra = ["cli", "lxml", "soap"] }, { name = "xsdata-pydantic", extra = ["cli", "lxml", "soap"] }, ] @@ -35,6 +36,7 @@ requires-dist = [ { name = "slowapi", specifier = ">=0.1.9" }, { name = "sqlalchemy", specifier = ">=2.0.43" }, { name = "uvicorn", specifier = ">=0.37.0" }, + { name = "voluptuous", specifier = ">=0.15.2" }, { name = "xsdata", extras = ["cli", "lxml", "soap"], specifier = ">=25.7" }, { name = "xsdata-pydantic", extras = ["cli", "lxml", "soap"], specifier = ">=24.5" }, ]