""" Central place for all configuration. Settings live in a .env file at the project root. This module wraps python-dotenv so the rest of the app never touches the file directly - that keeps the settings dialog, the services, and the database layer all agreeing on where config comes from. As we add ShipStation / Odoo / anything else, add the new keys to DEFAULTS below and to .env.example - everything else (loading, saving, the settings dialog) will pick them up automatically because the dialog is built from this list. """ from __future__ import annotations import os from pathlib import Path from typing import Dict from dotenv import dotenv_values, set_key # Project root = the folder containing this app/ package PROJECT_ROOT = Path(__file__).resolve().parent.parent ENV_PATH = PROJECT_ROOT / ".env" # Every setting the app knows about, grouped for the settings UI. # key -> (label, group, is_secret) SETTINGS_SCHEMA: Dict[str, tuple[str, str, bool]] = { "DB_URL": ("Database URL", "Database", False), "JIRA_URL": ("JIRA Site URL", "JIRA", False), "JIRA_EMAIL": ("JIRA Account Email", "JIRA", False), "JIRA_API_TOKEN": ("JIRA API Token", "JIRA", True), "JIRA_JQL": ("JIRA Query (JQL)", "JIRA", False), "JIRA_SIGNIFY_SKU_FIELDS": ( "Signify Health JIRA Deliverable Field IDs (comma-separated)", "JIRA", False, ), "JIRA_OAKSTREET_SKU_FIELDS": ( "Oak Street Health JIRA Deliverable Field IDs (comma-separated)", "JIRA", False, ), "SHIPSTATION_API_KEY": ("ShipStation API Key", "ShipStation", True), "SHIPSTATION_STORE_MAP": ( "Store ID -> Company (e.g. 123456:Signify Health,789012:Oak Street Health)", "ShipStation", False, ), "COMPANY_SKU_MAP": ( "SKU Prefix -> Company (e.g. SH:Signify Health,OK:Oak Street Health)", "Companies", False, ), "TICKET_NUMBER_REGEX": ( "Ticket Number Pattern (regex, e.g. AR-######)", "Companies", False, ), } DEFAULT_DB_URL = "sqlite:///orders.db" DEFAULT_COMPANY_SKU_MAP = "SH:Signify Health,OK:Oak Street Health" DEFAULT_TICKET_NUMBER_REGEX = r"\b[A-Z]{2,6}-\d{3,}\b" def ensure_env_file_exists() -> None: """Create a .env from .env.example on first run if one doesn't exist yet.""" if ENV_PATH.exists(): return example = PROJECT_ROOT / ".env.example" if example.exists(): ENV_PATH.write_text(example.read_text()) else: ENV_PATH.touch() def load_settings() -> Dict[str, str]: """Read current values from .env (does not touch os.environ).""" ensure_env_file_exists() values = dotenv_values(ENV_PATH) # Fill in blanks for any known key that's missing from the file return {key: values.get(key) or "" for key in SETTINGS_SCHEMA} def save_settings(values: Dict[str, str]) -> None: """Write settings back to .env, one key at a time (preserves the file).""" ensure_env_file_exists() for key, value in values.items(): if key in SETTINGS_SCHEMA: set_key(str(ENV_PATH), key, value or "") # Refresh the current process's environment too, so a running app # picks up the change without needing a restart for most settings. for key, value in values.items(): os.environ[key] = value or "" def get(key: str, default: str = "") -> str: """Convenience getter, e.g. config.get('DB_URL').""" return load_settings().get(key, default) or default