"""Explicit accepted-source composition; no default source or credential loader. Supplying components does not certify Oracle report semantics. A reviewed launcher owns that acceptance and the reader factory's credential lifetime. """ from dataclasses import dataclass from pathlib import Path from typing import Callable from arr_web.arr_download_executor import ARRMappingValidator, ARRSourceAdapter, CapturedARRExecutor from arr_web.arr_downloads import PersistentARRDownloads from arr_web.arr_data_runtime import DirectARRSource, compose_direct_arr_downloads from arr_web.processing_runtime import ProcessingInputRuntime from integrations.ohip.audit_arr_capture import protected_read from integrations.ohip.capture_job import atomic_json, fingerprint, job_lock, private_directory, sync_directory from integrations.ohip.collect_arr_source import APPLICATION, SERVICE, require, strict_json @dataclass(frozen=True) class CapturedARRSource: root: Path hotel_id: str adapter_contract: str adapter: ARRSourceAdapter mapping_validator: ARRMappingValidator reader_factory: Callable capture_version: str = "v2" max_profiles: int | None = None page_size: int = 100 max_pages: int = 100 max_records: int = 10000 def compose_arr_downloads(source: CapturedARRSource | DirectARRSource, processing: ProcessingInputRuntime) -> PersistentARRDownloads: if isinstance(source, DirectARRSource): return compose_direct_arr_downloads(source, processing) root = Path(source.root).absolute() # Constructor validates contracts, path and bounds before creating a queue. # Reuse upload dependencies, including the exact loaded processing policy. executor = CapturedARRExecutor( root=root / "acquisition", hotel_id=source.hotel_id, adapter_contract=source.adapter_contract, adapter=source.adapter, mapping_validator=source.mapping_validator, reader_factory=source.reader_factory, capture_version=source.capture_version, max_profiles=source.max_profiles, page_size=source.page_size, max_pages=source.max_pages, max_records=source.max_records, policy=processing.policy, object_store=processing.object_store, repository=processing.repository, ingestion=processing.ingestion, processor=processing.processor, ) private_directory(root) sync_directory(root.parent) identity = { "version": "arr-download-runtime/v1", "hotel_id": source.hotel_id, "service_url": SERVICE, "application_id": APPLICATION, "adapter_contract": source.adapter_contract, "capture_version": source.capture_version, "max_profiles": source.max_profiles, "page_size": source.page_size, "max_pages": source.max_pages, "max_records": source.max_records, "processor_version": processing.policy.processor_version, "rule_set_sha256": processing.policy.rule_set_sha256, } # Pin before dispatch, including jobs queued before their first capture. A # deployment cannot silently rebind those jobs to a different hotel/policy. with job_lock(root): pin = root / "source.json" if not pin.exists(): require(not (root / "queue").exists() and not (root / "acquisition").exists(), "unbound_download_runtime_state") atomic_json(pin, identity, replace=False) require(fingerprint(strict_json(protected_read(pin, 65536))) == fingerprint(identity), "download_runtime_source_conflict") return PersistentARRDownloads(root / "queue", executor)