Files

92 lines
5.1 KiB
Python

"""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