"""Generated from openapi.json; configure credentials only in your backend.""" from __future__ import annotations import hashlib import hmac import json from typing import Any, Callable, Literal, NotRequired, TypedDict import urllib.error import urllib.parse import urllib.request class CaseHistory(TypedDict): items: list[dict[str, Any]] next: int | None class CasePage(TypedDict): items: list[CaseView] next: str | None class CaseView(TypedDict): case_id: str app_key: str major_version: int revision: int status: str version: dict[str, Any] input: EvaluationRequest prediction: EvaluationResult | None decision: dict[str, Any] | None error_code: str | None replayed: NotRequired[bool] class Column(TypedDict): key: str type: Literal['string', 'decimal', 'integer', 'boolean'] nullable: NotRequired[bool] unit: NotRequired[str | None] tolerance: NotRequired[str] class Difference(TypedDict): key: list[JsonValue] column: str expected: JsonValue actual: JsonValue class EvaluationRequest(TypedDict): snapshot: dict[str, Any] class EvaluationResult(TypedDict): state: Literal['equal', 'different', 'unverified'] expected_rows: int actual_rows: int expected_digest: str actual_digest: str differences: list[Difference] reason: NotRequired[str | None] missing_rows: NotRequired[int | None] extra_rows: NotRequired[int | None] duplicate_expected: NotRequired[int | None] duplicate_actual: NotRequired[int | None] different_values: NotRequired[int | None] differences_truncated: NotRequired[bool | None] class ExtractResult(TypedDict): run_id: str document_version_id: str parser_revision: str complete: bool issues: list[dict[str, JsonValue]] fields: list[dict[str, JsonValue]] rows: list[dict[str, JsonValue]] decision: Literal['pending'] document_type: str template_version: int usage: ExtractionUsage customer_certified: NotRequired[Literal[False]] class ExtractionUsage(TypedDict): documents: int pages: int llm_tokens: int llm_tokens_estimated: int tokens_exact: bool class HTTPValidationError(TypedDict): detail: NotRequired[list[ValidationError]] JsonValue = Any class PoRow(TypedDict): key: str po_number: str supplier: str currency: str remaining: str class ResolveResult(TypedDict): mode: NotRequired[Literal['isolated_replay']] state: Literal['matched', 'unmatched', 'ambiguous', 'insufficient_remaining'] candidates: list[PoRow] reservation_created: NotRequired[Literal[False]] class Snapshot(TypedDict): grain: str primary_key: list[str] columns: list[Column] rows: list[dict[str, Any]] complete: bool class SourceQueryResult(TypedDict): columns: list[str] rows: list[list[JsonValue]] mode: Literal['workload', 'delegated_oauth', 'kerberos'] class ValidationError(TypedDict): loc: list[str | int] msg: str type: str input: NotRequired[Any] ctx: NotRequired[dict[str, Any]] class CaseSubmission(TypedDict): input: dict[str, Any] class CaseMutation(TypedDict): expected_revision: int class CaseDecision(TypedDict): expected_revision: int decision: Literal['approve', 'reject', 'correct'] reason: str correction: NotRequired[dict[str, Any] | None] golden_consent: NotRequired[bool] class CompareRequest(TypedDict): expected: dict[str, Any] actual: dict[str, Any] class ResolveRequest(TypedDict): snapshot: dict[str, Any] invoice: dict[str, Any] class QueryRequest(TypedDict): app_key: str major_version: int query_key: str after: NotRequired[str] class NotifyRequest(TypedDict): app_key: str major_version: int case_id: str expected_revision: int class ExtractRequest(TypedDict): document_type: str mime_type: Literal['application/pdf', 'image/png', 'image/jpeg'] content_base64: str inputs: NotRequired[dict[str, Any]] class SourceQueryRequest(TypedDict): query_key: str parameters: NotRequired[dict[str, Any]] identity_session: NotRequired[str | None] class ApiError(RuntimeError): """Expose HTTP status and Retry-After without echoing credentials or request contents.""" def __init__(self, status: int, retry_after: str | None): super().__init__(f'AgentData API returned HTTP {status}') self.status = status self.retry_after = retry_after class _NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self, request, fp, code, msg, headers, newurl): """Refuse credential-bearing redirects to any destination.""" return None class AgentDataClient: """Server-side HTTPS client; credentials and fresh subject proofs stay with the caller.""" def __init__(self, *, base_url: str, credential: Callable[[], str], timeout_seconds: float, max_response_bytes: int, subject: Callable[[], str | None] | None = None): parsed = urllib.parse.urlsplit(base_url) if (parsed.scheme != 'https' or not parsed.hostname or parsed.username or parsed.password or parsed.query or parsed.fragment or timeout_seconds <= 0 or max_response_bytes <= 0): raise ValueError('Configure an HTTPS origin and positive timeout/response limits') self.base_url = base_url.rstrip('/') self.credential = credential self.subject = subject self.timeout_seconds = timeout_seconds self.max_response_bytes = max_response_bytes self.opener = urllib.request.build_opener(_NoRedirect()) def _request(self, method: str, path: str, *, body=None, headers=None, query=None): endpoint = self.base_url + path if query: endpoint += '?' + urllib.parse.urlencode(query) outgoing = {'Authorization': 'Bearer ' + self.credential(), 'Accept': 'application/json'} proof = self.subject() if self.subject else None if proof: outgoing['X-AgentData-Subject'] = proof outgoing.update(headers or {}) data = None if body is not None: outgoing['Content-Type'] = 'application/json' data = json.dumps(body, allow_nan=False, separators=(',', ':')).encode() request = urllib.request.Request(endpoint, data, outgoing, method=method) try: with self.opener.open(request, timeout=self.timeout_seconds) as response: content = response.read(self.max_response_bytes + 1) if len(content) > self.max_response_bytes: raise ValueError('API response exceeds configured limit') return json.loads(content) except urllib.error.HTTPError as error: status, retry = error.code, error.headers.get('Retry-After') error.close() raise ApiError(status, retry) from None def session(self): """GET /app-api/v1/session — generated operation.""" return self._request('GET', f'/app-api/v1/session', body=None, query={key:value for key,value in {}.items() if value is not None}, headers={}) def app_schema(self, *, app_key: str, major: int): """GET /app-api/apps/ — generated operation.""" return self._request('GET', f"/app-api/apps/{urllib.parse.quote(str(app_key), safe='')}/v{urllib.parse.quote(str(major), safe='')}/openapi.json", body=None, query={key:value for key,value in {}.items() if value is not None}, headers={}) def app_evaluate(self, *, app_key: str, major: int, body: EvaluationRequest): """POST /app-api/apps/ — generated operation.""" return self._request('POST', f"/app-api/apps/{urllib.parse.quote(str(app_key), safe='')}/v{urllib.parse.quote(str(major), safe='')}/evaluate", body=body, query={key:value for key,value in {}.items() if value is not None}, headers={}) def submit(self, *, app_key: str, major: int, idempotency_key: str, body: CaseSubmission): """POST /app-api/apps/ — generated operation.""" return self._request('POST', f"/app-api/apps/{urllib.parse.quote(str(app_key), safe='')}/v{urllib.parse.quote(str(major), safe='')}/cases", body=body, query={key:value for key,value in {}.items() if value is not None}, headers={'Idempotency-Key': idempotency_key}) def listing(self, *, app_key: str, major: int, after: str | None = None): """GET /app-api/apps/ — generated operation.""" return self._request('GET', f"/app-api/apps/{urllib.parse.quote(str(app_key), safe='')}/v{urllib.parse.quote(str(major), safe='')}/cases", body=None, query={key:value for key,value in {'after': after}.items() if value is not None}, headers={}) def read(self, *, app_key: str, major: int, case_id: str): """GET /app-api/apps/ — generated operation.""" return self._request('GET', f"/app-api/apps/{urllib.parse.quote(str(app_key), safe='')}/v{urllib.parse.quote(str(major), safe='')}/cases/{urllib.parse.quote(str(case_id), safe='')}", body=None, query={key:value for key,value in {}.items() if value is not None}, headers={}) def history(self, *, app_key: str, major: int, case_id: str, after: int | None = None): """GET /app-api/apps/ — generated operation.""" return self._request('GET', f"/app-api/apps/{urllib.parse.quote(str(app_key), safe='')}/v{urllib.parse.quote(str(major), safe='')}/cases/{urllib.parse.quote(str(case_id), safe='')}/history", body=None, query={key:value for key,value in {'after': after}.items() if value is not None}, headers={}) def cancel(self, *, app_key: str, major: int, case_id: str, body: CaseMutation): """POST /app-api/apps/ — generated operation.""" return self._request('POST', f"/app-api/apps/{urllib.parse.quote(str(app_key), safe='')}/v{urllib.parse.quote(str(major), safe='')}/cases/{urllib.parse.quote(str(case_id), safe='')}/cancel", body=body, query={key:value for key,value in {}.items() if value is not None}, headers={}) def retry(self, *, app_key: str, major: int, case_id: str, body: CaseMutation): """POST /app-api/apps/ — generated operation.""" return self._request('POST', f"/app-api/apps/{urllib.parse.quote(str(app_key), safe='')}/v{urllib.parse.quote(str(major), safe='')}/cases/{urllib.parse.quote(str(case_id), safe='')}/retry", body=body, query={key:value for key,value in {}.items() if value is not None}, headers={}) def decide(self, *, app_key: str, major: int, case_id: str, body: CaseDecision): """POST /app-api/apps/ — generated operation.""" return self._request('POST', f"/app-api/apps/{urllib.parse.quote(str(app_key), safe='')}/v{urllib.parse.quote(str(major), safe='')}/cases/{urllib.parse.quote(str(case_id), safe='')}/decisions", body=body, query={key:value for key,value in {}.items() if value is not None}, headers={}) def golden(self, *, app_key: str, major: int, case_id: str): """GET /app-api/apps/ — generated operation.""" return self._request('GET', f"/app-api/apps/{urllib.parse.quote(str(app_key), safe='')}/v{urllib.parse.quote(str(major), safe='')}/cases/{urllib.parse.quote(str(case_id), safe='')}/golden-candidate", body=None, query={key:value for key,value in {}.items() if value is not None}, headers={}) def compare(self, *, body: CompareRequest): """POST /app-api/v1/capabilities/compare — generated operation.""" return self._request('POST', f'/app-api/v1/capabilities/compare', body=body, query={key:value for key,value in {}.items() if value is not None}, headers={}) def resolve(self, *, body: ResolveRequest): """POST /app-api/v1/capabilities/resolve — generated operation.""" return self._request('POST', f'/app-api/v1/capabilities/resolve', body=body, query={key:value for key,value in {}.items() if value is not None}, headers={}) def query(self, *, body: QueryRequest): """POST /app-api/v1/capabilities/query — generated operation.""" return self._request('POST', f'/app-api/v1/capabilities/query', body=body, query={key:value for key,value in {}.items() if value is not None}, headers={}) def notify(self, *, body: NotifyRequest): """POST /app-api/v1/capabilities/notify — generated operation.""" return self._request('POST', f'/app-api/v1/capabilities/notify', body=body, query={key:value for key,value in {}.items() if value is not None}, headers={}) def extract(self, *, body: ExtractRequest): """POST /app-api/v1/capabilities/extract — generated operation.""" return self._request('POST', f'/app-api/v1/capabilities/extract', body=body, query={key:value for key,value in {}.items() if value is not None}, headers={}) def source_query(self, *, body: SourceQueryRequest): """POST /app-api/v1/capabilities/query/source — generated operation.""" return self._request('POST', f'/app-api/v1/capabilities/query/source', body=body, query={key:value for key,value in {}.items() if value is not None}, headers={}) def saved_query(self, *, app_key: str, major: int, query_key: str, after: str | None = None): """GET /app-api/apps/ — generated operation.""" return self._request('GET', f"/app-api/apps/{urllib.parse.quote(str(app_key), safe='')}/v{urllib.parse.quote(str(major), safe='')}/queries/{urllib.parse.quote(str(query_key), safe='')}", body=None, query={key:value for key,value in {'after': after}.items() if value is not None}, headers={}) def verify_webhook(*, secret: str, timestamp: str, event_id: str, body: bytes, signature: str, now: int, replay_seconds: int) -> bool: """Verify exact bytes and timestamp; persist event_id separately to reject duplicate delivery.""" if replay_seconds <= 0: raise ValueError('Configure a positive replay window') try: stamp = int(timestamp) except ValueError: return False message = str(stamp).encode() + b'.' + event_id.encode() + b'.' + body expected = 'v1=' + hmac.new(secret.encode(), message, hashlib.sha256).hexdigest() return abs(now - stamp) <= replay_seconds and hmac.compare_digest(expected, signature)