#!/usr/bin/env python3 """ config.py - laedt die Config und stellt die Werte bereit. WICHTIG - was der Modus (test|prod) aendert: NUR die FORMULARDATEN sind pro Modus verschieden: contact -> Step 1 (Kontaktformular-Daten: Name, Telefon, Nachricht) application -> Step 3 (Bewerbungsformular / Mieter-Selbstauskunft) ALLES ANDERE ist GETEILT und in BEIDEN Modi ECHT (wird NICHT gemockt): email -> Step 2 (mail.tm / IMAP) -> echte Adresse + echter Token run -> Browser/Parallelitaet (headless, parallel_workers, ...) Es wird also NICHTS an der Funktionalitaet gemockt. E-Mail und Token sind IMMER echt. Der Modus bestimmt lediglich, welche Formular-Testdaten bzw. echten Bewerbungsdaten eingetragen werden. test -> Formulardaten = Dummy/Testdaten prod -> Formulardaten = deine echten Bewerbungsdaten Dateien: config/email.json -> "email" (GETEILT) config/run.json -> "run" (GETEILT) config//contact.json -> "contact" (pro Modus) config//application.json -> "application" (pro Modus) Aktiver Modus: ENV IBO_MODE (test|prod) hat Vorrang, sonst config/active_mode.txt (via UI umschaltbar), sonst 'prod'. """ import os import json HERE = os.path.dirname(os.path.abspath(__file__)) CONFIG_DIR = os.path.join(HERE, "config") MODES = ("test", "prod") DEFAULT_MODE = "prod" MODE_STATE_PATH = os.path.join(CONFIG_DIR, "active_mode.txt") # Geteilte Sektionen (fuer ALLE Modi gleich, echt): Datei im CONFIG_DIR. SHARED_FILE = { "email": "email.json", # Step 2 (Mail) - echt "run": "run.json", # Browser/Parallelitaet } # Pro-Modus-Sektionen (nur Formulardaten): Datei im Modus-Ordner. MODE_FILE = { "contact": "contact.json", # Step 1 "application": "application.json", # Step 3 } # Alte Ablagen: fuer Migration/Seed. (a) frueheres config//stepN.json, # (b) noch aeltere flache config_stepN.json, (c) alte gemeinsame config.json. LEGACY_MODE_STEPFILE = { # frueheres Schema (stepN pro Modus) "contact": "step1.json", "email": "step2.json", "application": "step3.json", "run": "run.json", } LEGACY_FLAT = { "contact": os.path.join(HERE, "config_step1.json"), "email": os.path.join(HERE, "config_step2.json"), "application": os.path.join(HERE, "config_step3.json"), "run": os.path.join(HERE, "config_run.json"), } LEGACY_COMBINED = os.path.join(HERE, "config.json") _HINWEIS = { "contact": "Step 1 – Kontaktformular (PRO MODUS). 'email' wird pro URL " "automatisch (mail.tm) erzeugt - leer lassen.", "email": "Step 2 – Mail (GETEILT, in test UND prod identisch/echt). " "mode: 'mailtm' (Wegwerf) oder 'imap'. sender_filter = erwarteter Absender.", "application": "Step 3 – Bewerbungsformular (PRO MODUS). reserveNumber, validUntil, " "numberRooms, secondTenant, consent, tenant1/tenant2 (Selbstauskunft).", "run": "App-Einstellungen (GETEILT). headless, parallel_workers, auto_submit, " "keep_browser_open.", } # --------------------------------------------------------------------------- # Eingebaute Defaults # --------------------------------------------------------------------------- # GETEILT (echt) - identisch fuer test und prod. SHARED_DEFAULTS = { "email": { "mode": "mailtm", "sender_filter": "muenchner-wohnen", "imap_host": "imap.gmail.com", "imap_user": "", "imap_password": "", }, "run": {"headless": False, "parallel_workers": 3, "auto_submit": False, "keep_browser_open": True}, } # PRO MODUS - nur Formulardaten (contact + application). FORM_DEFAULTS = { "prod": { "contact": { "salutation": "mr", "firstName": "Max", "lastName": "Mustermann", "phone": "", "email": "", "message": "Hallo, ich interessiere mich fuer das Objekt und wuerde mich " "ueber einen Besichtigungstermin freuen. Bitte kontaktieren Sie " "mich unter den angegebenen Daten. Vielen Dank!", }, "application": { "consent": True, "numberRooms": "3", "reserveNumber": "RES-12345", "secondTenant": True, "validUntil": "31.12.2026", "tenant1": {"firstName": "Max", "lastName": "Mustermann", "dateOfBirth": "01.01.1990", "street": "Hauptstrasse", "housenumber": "12", "zipcode": "80331", "city": "Muenchen", "phonePrivate": "", "phoneBusiness": "0891234567"}, "tenant2": {"firstName": "Erika", "lastName": "Mustermann", "dateOfBirth": "02.02.1992", "street": "Hauptstrasse", "housenumber": "12", "zipcode": "80331", "city": "Muenchen", "phonePrivate": "", "phoneBusiness": ""}, }, }, "test": { "contact": { "salutation": "mr", "firstName": "Test", "lastName": "Tester", "phone": "0000000000", "email": "", "message": "TEST – automatische Testnachricht.", }, "application": { "consent": True, "numberRooms": "2", "reserveNumber": "TEST-000", "secondTenant": True, "validUntil": "31.12.2099", "tenant1": {"firstName": "Test", "lastName": "Eins", "dateOfBirth": "01.01.2000", "street": "Teststrasse", "housenumber": "1", "zipcode": "10000", "city": "Teststadt", "phonePrivate": "0000000001", "phoneBusiness": "0000000002"}, "tenant2": {"firstName": "Test", "lastName": "Zwei", "dateOfBirth": "02.02.2000", "street": "Teststrasse", "housenumber": "2", "zipcode": "10000", "city": "Teststadt", "phonePrivate": "0000000003", "phoneBusiness": "0000000004"}, }, }, } # --------------------------------------------------------------------------- # Hilfsfunktionen # --------------------------------------------------------------------------- def _read_json(path): try: with open(path, encoding="utf-8") as f: return json.load(f) except Exception: return {} def _mode_dir(mode): return os.path.join(CONFIG_DIR, mode) def _valid_mode(mode): mode = (mode or "").strip().lower() if mode == "mock": # alter Name -> neuer Name mode = "test" return mode if mode in MODES else DEFAULT_MODE def current_mode(): """Aktiver Modus: ENV IBO_MODE hat Vorrang, sonst persistierte Datei, sonst Default.""" env = os.environ.get("IBO_MODE") if env: return _valid_mode(env) if os.path.exists(MODE_STATE_PATH): try: with open(MODE_STATE_PATH, encoding="utf-8") as f: return _valid_mode(f.read()) except Exception: pass return DEFAULT_MODE def _write(path, section, value): os.makedirs(os.path.dirname(path), exist_ok=True) out = {"_hinweis": _HINWEIS.get(section, ""), section: value} with open(path, "w", encoding="utf-8") as f: json.dump(out, f, indent=2, ensure_ascii=False) def _seed_from_chain(section, sources, default): """Erste Quelle nehmen, die die Sektion enthaelt; sonst default.""" for src in sources: data = _read_json(src) if section in data: return data[section] return default def _seed_shared(): """Geteilte Dateien (email, run) sicherstellen. Seed-Reihenfolge: fruehere Modus-Datei (prod) -> flache Legacy -> alte config.json -> Default.""" os.makedirs(CONFIG_DIR, exist_ok=True) for section, fname in SHARED_FILE.items(): path = os.path.join(CONFIG_DIR, fname) if os.path.exists(path): continue sources = [ os.path.join(_mode_dir("prod"), LEGACY_MODE_STEPFILE[section]), LEGACY_FLAT[section], LEGACY_COMBINED, ] _write(path, section, _seed_from_chain(section, sources, SHARED_DEFAULTS[section])) def _seed_mode(mode): """Pro-Modus-Dateien (contact, application) sicherstellen.""" os.makedirs(_mode_dir(mode), exist_ok=True) for section, fname in MODE_FILE.items(): path = os.path.join(_mode_dir(mode), fname) if os.path.exists(path): continue sources = [os.path.join(_mode_dir(mode), LEGACY_MODE_STEPFILE[section])] if mode == "prod": # prod darf auch aus den flachen Legacy-Dateien seeden sources += [LEGACY_FLAT[section], LEGACY_COMBINED] default = FORM_DEFAULTS[mode][section] _write(path, section, _seed_from_chain(section, sources, default)) # --------------------------------------------------------------------------- # Laden / Speichern # --------------------------------------------------------------------------- MODE = current_mode() def load(): """Geteilte (email, run) + Pro-Modus (contact, application) zu einer CFG.""" _seed_shared() _seed_mode(MODE) cfg = {} for section, fname in SHARED_FILE.items(): data = _read_json(os.path.join(CONFIG_DIR, fname)) cfg[section] = data.get(section, SHARED_DEFAULTS[section]) for section, fname in MODE_FILE.items(): data = _read_json(os.path.join(_mode_dir(MODE), fname)) cfg[section] = data.get(section, FORM_DEFAULTS[MODE][section]) return cfg def save(cfg): """email/run -> geteilte Dateien; contact/application -> Modus-Dateien.""" for section, fname in SHARED_FILE.items(): if section in cfg: _write(os.path.join(CONFIG_DIR, fname), section, cfg[section]) for section, fname in MODE_FILE.items(): if section in cfg: _write(os.path.join(_mode_dir(MODE), fname), section, cfg[section]) CFG = load() def reload(): global MODE, CFG MODE = current_mode() CFG = load() return CFG def set_mode(mode): """Aktiven Modus umschalten (persistiert) und Config neu laden. ENV IBO_MODE hat weiterhin Vorrang.""" global MODE, CFG mode = _valid_mode(mode) os.makedirs(CONFIG_DIR, exist_ok=True) with open(MODE_STATE_PATH, "w", encoding="utf-8") as f: f.write(mode) MODE = current_mode() CFG = load() return MODE def mode_locked_by_env(): """True, wenn der Modus per ENV festgelegt ist (UI-Umschalten dann gesperrt).""" return bool(os.environ.get("IBO_MODE"))