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