feat: add date-based OHIP data reporting alongside XML
This commit is contained in:
1 parent
5ecd571e3b
commit
ad2c59256f
259 files changed
+37326
-126
No files matched your search
@@ -0,0 +1,91 @@
|
||||
"""Bounded exact-ID summaries for v3 capture; no native ARR display claim."""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
import urllib.error
|
||||
|
||||
from . import collect_arr_source as source, profile_summary as probe
|
||||
|
||||
|
||||
class ProfileSummaryReader:
|
||||
def __init__(self, archive, hotel_id, transport, *, max_profiles: int, key="", sleep=time.sleep):
|
||||
source.Options("2000-01-01", hotel_id).validate()
|
||||
source.require(type(max_profiles) is int and 1 <= max_profiles <= 10000, "invalid_profile_limit")
|
||||
self.archive, self.hotel_id, self.transport = archive, hotel_id, transport
|
||||
self.max_profiles, self._key, self.sleep = max_profiles, key, sleep
|
||||
self.request_count = 0
|
||||
self._cache: dict[str, tuple[bytes, str]] = {}
|
||||
|
||||
@property
|
||||
def unique_profiles(self):
|
||||
return len(self._cache)
|
||||
|
||||
def read(self, profile_id: str) -> tuple[bytes, str]:
|
||||
method, path, payload = probe.request(profile_id)
|
||||
if profile_id in self._cache:
|
||||
return self._cache[profile_id]
|
||||
source.require(self.unique_profiles < self.max_profiles, "profile_limit_exceeded")
|
||||
for attempt in range(1, 4):
|
||||
self.request_count += 1
|
||||
prefix = f"profile-{self.request_count:06d}"
|
||||
request_file = prefix + ".json"
|
||||
self.archive.write(request_file, {"operation_id": probe.OPERATION, "method": method,
|
||||
"path": path, "body": source.strict_json(payload), "attempt": attempt, "started_at": source.utc_now()})
|
||||
try:
|
||||
status, headers, raw = self.transport(method, path, payload)
|
||||
except (urllib.error.URLError, TimeoutError, ConnectionError):
|
||||
self.archive.write(prefix + ".meta.json", {"error": "transport_failure", "ended_at": source.utc_now()})
|
||||
if attempt == 3:
|
||||
raise source.CollectionError("profile_transport_retry_exhausted") from None
|
||||
self.sleep(float(2 ** (attempt - 1)))
|
||||
continue
|
||||
source.require(not self._key or self._key.encode() not in raw, "secret_in_response")
|
||||
headers = {name.lower(): value for name, value in headers.items()}
|
||||
self.archive.write(prefix + ".response.bin", raw)
|
||||
self.archive.write(prefix + ".meta.json", {"http_status": status, "ended_at": source.utc_now(),
|
||||
"oversized": len(raw) > source.MAX_RESPONSE_BYTES,
|
||||
"edge_request_id": headers.get("x-request-id"), "retry_after": headers.get("retry-after")})
|
||||
source.require(len(raw) <= source.MAX_RESPONSE_BYTES, "profile_response_too_large")
|
||||
if status in source.RETRY_STATUSES and attempt < 3:
|
||||
self.sleep(source.retry_delay(headers, attempt))
|
||||
continue
|
||||
source.require(status == 200, "profile_http_failure")
|
||||
# Wrong identity, hotel, operation, warnings or pagination are fatal.
|
||||
probe.summary_row(raw, profile_id, self.hotel_id)
|
||||
self._cache[profile_id] = (raw, request_file)
|
||||
return self._cache[profile_id]
|
||||
raise source.CollectionError("profile_retry_exhausted")
|
||||
|
||||
|
||||
def assess(detail: dict, reader: ProfileSummaryReader) -> dict:
|
||||
"""Return private per-reservation evidence; intrinsic name gaps stay explicit."""
|
||||
try:
|
||||
guest = probe.primary_guest(detail, include_prefix=True)
|
||||
except source.CollectionError as error:
|
||||
if str(error) not in {"profile_missing_guest_list", "profile_ambiguous_primary_guest",
|
||||
"profile_missing_primary_info", "profile_missing_person_names",
|
||||
"profile_ambiguous_primary_name"}:
|
||||
raise
|
||||
return {"error": str(error)}
|
||||
raw, request_file = reader.read(guest.profile_id)
|
||||
evidence = {"profile_id": guest.profile_id, "profile_request": request_file}
|
||||
try:
|
||||
inspected = probe.inspect_summary(raw, guest, reader.hotel_id, include_prefix=True)
|
||||
evidence.update(name_type=inspected["name_type"],
|
||||
component_comparisons=inspected["component_comparisons"],
|
||||
full_name_state=inspected["full_name_state"])
|
||||
source.require(inspected["name_type"] in (None, "Primary"), "profile_nonprimary_summary_name")
|
||||
source.require(all(value in {"equal", "both_absent"}
|
||||
for value in inspected["component_comparisons"].values()), "profile_name_components_mismatch")
|
||||
source.require(any(guest.name.get(key, "").strip() for key in ("surname", "givenName", "middleName")),
|
||||
"profile_primary_name_unverifiable")
|
||||
source.require(inspected["full_name_state"] == "present", "profile_full_name_unavailable")
|
||||
except source.CollectionError as error:
|
||||
if str(error) not in {"profile_missing_summary_profile", "profile_missing_former_name",
|
||||
"profile_nonprimary_summary_name", "profile_name_components_mismatch",
|
||||
"profile_primary_name_unverifiable", "profile_full_name_unavailable"}:
|
||||
raise
|
||||
evidence["error"] = str(error)
|
||||
else:
|
||||
evidence["full_name"] = inspected["full_name"]
|
||||
return evidence
|
||||
Reference in new issue
Block a user