"""Isolated local HTTP acquisition → XML → Finance → monthly integration portal.""" import argparse from contextlib import contextmanager import os from pathlib import Path import signal from arr_web.arr_download_executor import CapturedARRExecutor from arr_web.local_api_fixture import HOTEL, VERSION, LocalAPIAdapter, NativeBaselineValidator, build_fixture from arr_web.local_api_server import serve_fixture from arr_web.local_replay import create_instance, open_instance from arr_web.local_xml_replay import LocalReplayPortal, NativeXMLSnapshot, document from arr_web.server import serve from integrations.ohip import collect_arr_source as source, rate_info from integrations.ohip.audit_arr_capture import protected_read from integrations.ohip.capture_job import atomic_json, fingerprint class LocalSimulationPortal(LocalReplayPortal): cookie_name = "arr_api_simulation_session" environment = "local-api-simulation" source_kind = "local_api_simulation" filename_prefix = "LOCAL-API-SIMULATION-" banner_title = "本机接口模拟 · ARR 全流程联调" banner_action = "点击下载会查询本机模拟接口、分页采集、生成 ARR,并进入独立测试数据库。" def simulation_manifest(snapshot, fixture): return {"version": VERSION, "source_sha256": snapshot.manifest["source_sha256"], "fixture_sha256": fingerprint(fixture), "hotel_id": HOTEL, "report_date": snapshot.day.isoformat(), "records": len(fixture["records"]), "oracle_connected": False, "synthetic_fields": ["hotelId", "Reservation ID", "reservationStatus", "lastModifyDateTime"], "local_report_fields": ["full_name", "company_name", "room_no", "block_code", "products", "room_category_label", "source_sequence"], "traces_selected": False, "oracle_report_equivalence_verified": False} def create_simulation(parent, source_path, expected_sha256, report_date): root = create_instance(parent, source_path, expected_sha256, report_date, prefix="arr-api-simulation-") snapshot = NativeXMLSnapshot.load(root / "fixture") fixture = build_fixture(snapshot) source.require(len(fixture["records"]) <= 2000, "local_simulation_record_limit") atomic_json(root / "simulation-data.json", fixture, replace=False) atomic_json(root / "simulation.json", simulation_manifest(snapshot, fixture), replace=False) return root @contextmanager def open_simulation(root, port): snapshot = NativeXMLSnapshot.load(root / "fixture") marker = document(root / "simulation.json") fixture = source.strict_json(protected_read(root / "simulation-data.json", 32 * 1024 * 1024)) expected_fixture = build_fixture(snapshot) source.require(fingerprint(marker) == fingerprint(simulation_manifest(snapshot, expected_fixture)) and fingerprint(fixture) == fingerprint(expected_fixture) and len(fixture["records"]) <= 2000, "local_simulation_identity_mismatch") with serve_fixture(fixture) as api: def factory(*, root, snapshot, **kwargs): def readers(archive, hotel): return (source.Reader(archive, hotel, api.transport, key=api.key), rate_info.RateInfoReader(archive, hotel, api.transport, key=api.key)) return CapturedARRExecutor(root=root / "acquisition", hotel_id=HOTEL, adapter_contract="local-api-simulation/v1-" + marker["fixture_sha256"], adapter=LocalAPIAdapter(marker["source_sha256"], marker["records"]), mapping_validator=NativeBaselineValidator(snapshot), reader_factory=readers, page_size=20, max_pages=100, max_records=2000, **kwargs) try: with open_instance(root, port, executor_factory=factory, portal_type=LocalSimulationPortal) as runtime: runtime.local_api = api yield runtime finally: atomic_json(root / "http-counts-last-run.json", {"environment": "local-api-simulation", "operations": dict(api.counts), "oracle_calls": 0}, replace=True) def main(argv=None): parser = argparse.ArgumentParser(description=__doc__) actions = parser.add_subparsers(dest="action", required=True) init = actions.add_parser("init") init.add_argument("--parent", type=Path, required=True) init.add_argument("--xml", type=Path, required=True) init.add_argument("--expected-sha256", required=True) init.add_argument("--report-date", required=True) run = actions.add_parser("serve") run.add_argument("--root", type=Path, required=True) run.add_argument("--port", type=int, default=8874) args = parser.parse_args(argv) os.umask(0o077) if args.action == "init": print(create_simulation(args.parent, args.xml, args.expected_sha256, args.report_date)) return def stop(_signal, _frame): raise KeyboardInterrupt signal.signal(signal.SIGTERM, stop) try: with open_simulation(args.root, args.port) as runtime: runtime.start_monthly_worker() print(f"Local API simulation only: 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()