"""Explicit synthetic-data portal for local acceptance. No Oracle transport or credentials.""" import argparse import hashlib from contextlib import contextmanager from datetime import date import os from pathlib import Path import secrets import signal import tempfile from types import SimpleNamespace from arr_processing.policy import load_processor_policy from arr_web.arr_data_executor import DirectARRExecutor from arr_web.local_replay import ReplayRuntime from arr_web.local_replay_database import ReplayDatabase from arr_web.local_xml_replay import LocalReplayPortal, document from arr_web.server import serve from integrations.ohip.arr_data import ARRDataSource from integrations.ohip.capture_job import atomic_json, fingerprint, job_lock, private_directory from tests.test_ohip_arr_data import SimulatedOHIP, DAY, HOTEL, FIXTURE PROJECT = Path(__file__).resolve().parents[1] class DirectDataPortal(LocalReplayPortal): allow_xml_upload = True cookie_name = "arr_direct_data_simulation_session" environment = "local-direct-data-simulation" source_kind = "synthetic_direct_data" filename_prefix = "LOCAL-DATA-" banner_title = "本机接口数据模拟 · ARR 全流程" banner_action = "直接处理 6 笔模拟订单,使用独立测试数据库。" def identity(root, policy): return {"version": "arr-local-direct-data/v1", "root": str(root), "hotel_id": HOTEL, "report_date": DAY, "source_records": 6, "oracle_connected": False, "fixture_sha256": hashlib.sha256(FIXTURE.read_bytes()).hexdigest(), "processor_version": policy.processor_version, "rule_set_sha256": policy.rule_set_sha256} def create(parent): if parent.resolve().is_relative_to(PROJECT): raise ValueError("local_data_must_be_outside_repository") root = Path(tempfile.mkdtemp(prefix="arr-direct-data-", dir=parent)).resolve() atomic_json(root / "instance.json", identity(root, load_processor_policy(PROJECT)), replace=False) atomic_json(root / "login.json", {"username": "arr-data", "password": secrets.token_urlsafe(24)}, replace=False) return root @contextmanager def open_portal(root, port): if root.is_symlink() or root.resolve().is_relative_to(PROJECT) or not 1024 <= port <= 65535: raise ValueError("invalid_local_instance") root = root.resolve(); private_directory(root) policy = load_processor_policy(PROJECT) if document(root / "instance.json") != identity(root, policy): raise ValueError("local_instance_source_or_rules_changed") snapshot = SimpleNamespace(day=date.fromisoformat(DAY), manifest={"source_records": 6}) with job_lock(root): database = ReplayDatabase(root, schema_version=19) try: database.start() def factory(*, root, snapshot, **dependencies): source = ARRDataSource(root / "source", HOTEL, transport_factory=lambda: SimulatedOHIP(6)) return DirectARRExecutor(root=root / "processing", source=source, **dependencies) runtime = ReplayRuntime(root, database, snapshot, policy, port, executor_factory=factory, portal_type=DirectDataPortal, enable_upload=True) try: yield runtime finally: runtime.close() finally: database.close() def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("action", choices=["init", "serve"]) parser.add_argument("--root", type=Path) parser.add_argument("--parent", type=Path, default=Path(tempfile.gettempdir())) parser.add_argument("--port", type=int, default=8875) args = parser.parse_args(); os.umask(0o077) if args.action == "init": print(create(args.parent)); return if args.root is None: parser.error("serve requires --root") def stop(*_): raise KeyboardInterrupt signal.signal(signal.SIGTERM, stop) try: with open_portal(args.root, args.port) as runtime: runtime.start_monthly_worker() print(f"Local direct-data simulation: http://127.0.0.1:{args.port}/", flush=True) serve(runtime.app, "127.0.0.1", args.port) except KeyboardInterrupt: pass if __name__ == "__main__": main()