Initial commit
This commit is contained in:
+103
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user