Module adcp.negotiation
Buyer-side verification helpers for AdCP proposal negotiation.
The generated models validate individual request and response shapes. This module verifies the cross-object invariants that JSON Schema cannot express: ordered result correlation, immutable lineage, RFC 8785 terms digests, alternative uniqueness, and the portable typed refinement constraints.
Global variables
var WIRE_RESPONSE_METADATA_KEY-
TaskResult metadata key containing the unnormalized refinement response.
Functions
def compute_terms_digest(commercial_terms: Mapping[str, Any]) ‑> str-
Expand source code
def compute_terms_digest(commercial_terms: Mapping[str, Any]) -> str: """Return the normative digest for a ``commercial_terms`` object. The digest is ``sha256:`` followed by unpadded base64url SHA-256 of the RFC 8785 JSON Canonicalization Scheme bytes. The input must be the original JSON object, not a parsed Pydantic model: model parsing can normalize date strings or inject defaults and therefore change the signed wire value. """ if not isinstance(commercial_terms, Mapping): raise TypeError("commercial_terms must be the original wire mapping") wire_terms = dict(commercial_terms) canonical = rfc8785.dumps(wire_terms) encoded = base64.urlsafe_b64encode(hashlib.sha256(canonical).digest()).rstrip(b"=") return "sha256:" + encoded.decode("ascii")Return the normative digest for a
commercial_termsobject.The digest is
sha256:followed by unpadded base64url SHA-256 of the RFC 8785 JSON Canonicalization Scheme bytes. The input must be the original JSON object, not a parsed Pydantic model: model parsing can normalize date strings or inject defaults and therefore change the signed wire value. def preflight_refine_proposals(request: BaseModel | Mapping[str, Any],
proposal_refinement: BaseModel | Mapping[str, Any] | None) ‑> NegotiationVerificationResult-
Expand source code
def preflight_refine_proposals( request: BaseModel | Mapping[str, Any], proposal_refinement: BaseModel | Mapping[str, Any] | None, ) -> NegotiationVerificationResult: """Check a refine request against an advertised capability declaration. A missing declaration means support is unknown and does not fail preflight. An explicit ``supported_dimensions`` list is authoritative; omitted typed dimensions are reported before transport. Free-text ``ask`` is intentionally not capability-gated. """ request_data = ( request.model_dump(mode="json", by_alias=True, exclude_none=True, exclude_unset=True) if isinstance(request, BaseModel) else _as_dict(request) ) if request_data is None: raise TypeError("request must serialize to a JSON object") try: _refine_request_adapter().validate_python(request_data) except ValidationError as exc: return NegotiationVerificationResult( tuple( NegotiationVerificationIssue( "invalid_request_schema", error["msg"], "/" + "/".join(str(part) for part in error["loc"]), ) for error in exc.errors(include_url=False) ) ) refinements = _as_list(request_data.get("refinements")) if refinements is None: return NegotiationVerificationResult( ( NegotiationVerificationIssue( "invalid_request_shape", "request refinements are not an ordered array", "/refinements", ), ) ) issues: list[NegotiationVerificationIssue] = [] proposal_ids: set[str] = set() actions: set[str] = set() capability = _as_dict(proposal_refinement) if proposal_refinement is not None else None supported = capability.get("supported_dimensions") if capability is not None else None supported_dimensions = set(supported) if isinstance(supported, list) else None max_alternatives = capability.get("max_alternatives") if capability is not None else None for index, refinement in enumerate(refinements): pointer = f"/refinements/{index}" if not isinstance(refinement, dict): issues.append( NegotiationVerificationIssue( "invalid_request_shape", "refinement must be an object", pointer ) ) continue proposal_id = refinement.get("proposal_id") if isinstance(proposal_id, str): if proposal_id in proposal_ids: issues.append( NegotiationVerificationIssue( "duplicate_proposal_id", "proposal_id values must be unique within the batch", f"{pointer}/proposal_id", ) ) proposal_ids.add(proposal_id) action = refinement.get("action") if isinstance(action, str): actions.add(action) if action not in {"revise", "finalize"}: issues.append( NegotiationVerificationIssue( "invalid_request_schema", "action must be explicitly set to revise or finalize", f"{pointer}/action", ) ) change_fields = { key for key in ("constraints", "product_changes", "alternatives", "ask", "criteria") if key in refinement } cancellation = refinement.get("change_kind") == "cancellation" if action == "revise" and not change_fields and not cancellation: issues.append( NegotiationVerificationIssue( "invalid_request_schema", "revise requires a typed change, ask, criteria, or cancellation", pointer, ) ) if action == "finalize": forbidden = change_fields | ({"change_kind"} if "change_kind" in refinement else set()) for key in sorted(forbidden): issues.append( NegotiationVerificationIssue( "invalid_request_schema", "finalize cannot include revision fields", f"{pointer}/{key}", ) ) requested_dimensions: list[tuple[str, str]] = [] constraints = refinement.get("constraints") if isinstance(constraints, dict): requested_dimensions.extend( (dimension, f"{pointer}/constraints/{dimension}") for dimension, value in constraints.items() if value is not None ) for dimension in ("product_changes", "alternatives", "criteria"): if refinement.get(dimension) is not None: requested_dimensions.append((dimension, f"{pointer}/{dimension}")) if supported_dimensions is not None: for dimension, dimension_pointer in requested_dimensions: if dimension not in supported_dimensions: issues.append( NegotiationVerificationIssue( "unsupported_dimension", f"seller does not advertise refinement dimension {dimension!r}", dimension_pointer, ) ) alternatives = refinement.get("alternatives") count = alternatives.get("count") if isinstance(alternatives, dict) else None if ( isinstance(count, int) and isinstance(max_alternatives, int) and count > max_alternatives ): issues.append( NegotiationVerificationIssue( "max_alternatives", f"requested {count} alternatives; seller maximum is {max_alternatives}", f"{pointer}/alternatives/count", ) ) if "finalize" in actions and actions != {"finalize"}: issues.append( NegotiationVerificationIssue( "mixed_finalize_batch", "a batch containing finalize must contain only finalize entries", "/refinements", ) ) return NegotiationVerificationResult(tuple(issues))Check a refine request against an advertised capability declaration.
A missing declaration means support is unknown and does not fail preflight. An explicit
supported_dimensionslist is authoritative; omitted typed dimensions are reported before transport. Free-textaskis intentionally not capability-gated. async def refine_proposals_verified(client: RefineProposalsClient,
request: BaseModel | Mapping[str, Any],
proposal_refinement: BaseModel | Mapping[str, Any] | None = None,
*,
source_proposals: Mapping[str, BaseModel | Mapping[str, Any]] | None = None,
now: datetime | None = None) ‑> VerifiedRefinementResult-
Expand source code
async def refine_proposals_verified( client: RefineProposalsClient, request: BaseModel | Mapping[str, Any], proposal_refinement: BaseModel | Mapping[str, Any] | None = None, *, source_proposals: Mapping[str, BaseModel | Mapping[str, Any]] | None = None, now: datetime | None = None, ) -> VerifiedRefinementResult: """Preflight, execute, and verify one proposal-refinement request. Preflight failures raise :class:`NegotiationVerificationError` before transport. A successful completed response is verified against the exact wire payload retained by :class:`~adcp.client.ADCPClient`; invalid seller output also raises. Task-level failures remain ordinary ``TaskResult`` values, with canonical unsupported-dimension recovery parsed alongside. Submitted/working results are returned without premature verification. """ preflight = preflight_refine_proposals(request, proposal_refinement) preflight.raise_for_errors() task_result = await client.refine_proposals(request) return verify_refinement_result( request, task_result, proposal_refinement=proposal_refinement, source_proposals=source_proposals, now=now, )Preflight, execute, and verify one proposal-refinement request.
Preflight failures raise :class:
NegotiationVerificationErrorbefore transport. A successful completed response is verified against the exact wire payload retained by :class:~adcp.client.ADCPClient; invalid seller output also raises. Task-level failures remain ordinaryTaskResultvalues, with canonical unsupported-dimension recovery parsed alongside. Submitted/working results are returned without premature verification. def unsupported_refinement_recovery(result: TaskResult[Any] | Mapping[str, Any]) ‑> UnsupportedRefinementRecovery | None-
Expand source code
def unsupported_refinement_recovery( result: TaskResult[Any] | Mapping[str, Any], ) -> UnsupportedRefinementRecovery | None: """Parse the canonical task-level unsupported-dimension error details. Unknown or malformed error-detail shapes return ``None`` so callers still handle the task failure through the normal ``TaskResult.adcp_error`` path. """ if isinstance(result, TaskResult): error = result.adcp_error else: candidate = result.get("adcp_error", result) error = candidate if isinstance(candidate, dict) else None if not isinstance(error, dict) or error.get("code") != "UNSUPPORTED_FEATURE": return None details = error.get("details") if not isinstance(details, dict): return None try: parsed = UnsupportedRefinementDimensionDetails.model_validate(details) except ValueError: return None return UnsupportedRefinementRecovery( unsupported_dimension=parsed.unsupported_dimension, supported_dimensions=tuple(str(item) for item in parsed.supported_dimensions), )Parse the canonical task-level unsupported-dimension error details.
Unknown or malformed error-detail shapes return
Noneso callers still handle the task failure through the normalTaskResult.adcp_errorpath. def verify_refine_proposals_response(request: BaseModel | Mapping[str, Any],
response: TaskResult[Any] | BaseModel | Mapping[str, Any],
*,
proposal_refinement: BaseModel | Mapping[str, Any] | None = None,
source_proposals: Mapping[str, BaseModel | Mapping[str, Any]] | None = None,
now: datetime | None = None) ‑> NegotiationVerificationResult-
Expand source code
def verify_refine_proposals_response( request: BaseModel | Mapping[str, Any], response: TaskResult[Any] | BaseModel | Mapping[str, Any], *, proposal_refinement: BaseModel | Mapping[str, Any] | None = None, source_proposals: Mapping[str, BaseModel | Mapping[str, Any]] | None = None, now: datetime | None = None, ) -> NegotiationVerificationResult: """Verify cross-object invariants for a completed refine response. Submitted/working responses without ``results`` are valid but have no proposal invariants to inspect. The helper never mutates either model. """ request_data = _as_dict(request) response_data, response_status, wire_faithful = _response_data(response) if request_data is None: raise TypeError("request must serialize to a JSON object") if response_data is None: if response_status in {TaskStatus.SUBMITTED, TaskStatus.WORKING, "submitted", "working"}: return NegotiationVerificationResult() if isinstance(response, TaskResult): return NegotiationVerificationResult( ( NegotiationVerificationIssue( "missing_results", "a completed refinement response must contain ordered results", "/results", ), ) ) raise TypeError("response must serialize to a JSON object") refinements = _as_list(request_data.get("refinements")) results = _as_list(response_data.get("results")) if results is None: if response_status in {TaskStatus.SUBMITTED, TaskStatus.WORKING, "submitted", "working"}: return NegotiationVerificationResult() return NegotiationVerificationResult( ( NegotiationVerificationIssue( "missing_results", "a completed refinement response must contain ordered results", "/results", ), ) ) if refinements is None: return NegotiationVerificationResult( ( NegotiationVerificationIssue( "invalid_request_shape", "request refinements are not an ordered array", "/refinements", ), ) ) issues: list[NegotiationVerificationIssue] = [] try: _refine_response_adapter().validate_python(response_data) except ValidationError as exc: issues.extend( NegotiationVerificationIssue( "invalid_response_schema", error["msg"], "/" + "/".join(str(part) for part in error["loc"]), ) for error in exc.errors(include_url=False) ) capability = _as_dict(proposal_refinement) if proposal_refinement is not None else None supported = capability.get("supported_dimensions") if capability is not None else None supported_dimensions = set(supported) if isinstance(supported, list) else None verification_time = now or datetime.now(timezone.utc) if verification_time.tzinfo is None or verification_time.utcoffset() is None: raise ValueError("now must be timezone-aware") if not wire_faithful: issues.append( NegotiationVerificationIssue( "wire_data_required", "digest verification requires the original response mapping; pass the " "TaskResult returned by ADCPClient or an unparsed wire response", "/results", ) ) if len(results) != len(refinements): issues.append( NegotiationVerificationIssue( "cardinality", f"expected {len(refinements)} ordered results, got {len(results)}", "/results", ) ) source_ids = { str(refinement.get("proposal_id")) for refinement in refinements if isinstance(refinement, dict) and isinstance(refinement.get("proposal_id"), str) } returned_proposal_ids: set[str] = set() outcomes = {result.get("outcome") for result in results if isinstance(result, dict)} if "finalized" in outcomes and outcomes != {"finalized"}: issues.append( NegotiationVerificationIssue( "mixed_finalize_batch", "a finalize batch must contain only finalized results", "/results", ) ) for index, (refinement, result) in enumerate(zip(refinements, results)): pointer = f"/results/{index}" if not isinstance(refinement, dict) or not isinstance(result, dict): issues.append( NegotiationVerificationIssue( "invalid_result_shape", "refinement and result must be objects", pointer ) ) continue source_id = refinement.get("proposal_id") if result.get("source_proposal_id") != source_id: issues.append( NegotiationVerificationIssue( "result_order", "source_proposal_id does not match the request entry at this position", f"{pointer}/source_proposal_id", ) ) action = refinement.get("action") outcome = result.get("outcome") reason_code = result.get("reason_code") allowed_outcomes = { "revise": {"revised", "partial", "unable"}, "finalize": {"finalized", "unable"}, } if action in allowed_outcomes and outcome not in allowed_outcomes[action]: issues.append( NegotiationVerificationIssue( "action_outcome_mismatch", f"action {action!r} cannot produce outcome {outcome!r}", f"{pointer}/outcome", ) ) ask = refinement.get("ask") if reason_code in {"commercially_declined", "uninterpreted"} and not ( isinstance(ask, str) and ask.strip() ): issues.append( NegotiationVerificationIssue( "reason_mismatch", f"reason_code {reason_code!r} requires a non-empty free-text ask", f"{pointer}/reason_code", ) ) if reason_code in {"hold_unavailable", "batch_aborted"} and action != "finalize": issues.append( NegotiationVerificationIssue( "reason_mismatch", f"reason_code {reason_code!r} is valid only for finalize", f"{pointer}/reason_code", ) ) if reason_code == "unsupported_dimension": requested_dimensions: set[str] = set() constraints = refinement.get("constraints") if isinstance(constraints, dict): requested_dimensions.update( key for key, value in constraints.items() if value is not None ) requested_dimensions.update( key for key in ("product_changes", "alternatives", "criteria") if refinement.get(key) is not None ) invalid_unsupported_reason = action != "revise" or not requested_dimensions if supported_dimensions is not None: invalid_unsupported_reason = invalid_unsupported_reason or ( requested_dimensions <= supported_dimensions ) if invalid_unsupported_reason: issues.append( NegotiationVerificationIssue( "reason_mismatch", "unsupported_dimension requires revise with a requested typed " "dimension outside the seller's advertised supported_dimensions", f"{pointer}/reason_code", ) ) proposals: list[dict[str, Any]] = [] plural = result.get("proposals") if isinstance(plural, list): proposals.extend(item for item in plural if isinstance(item, dict)) else: singular = result.get("proposal") if isinstance(singular, dict): proposals.append(singular) computed_digests: list[str] = [] failed_constraint_union: set[str] = set() failed_change_union: set[str] = set() for proposal_index, proposal in enumerate(proposals): proposal_pointer = f"{pointer}/proposals/{proposal_index}" proposal_id = proposal.get("proposal_id") if isinstance(proposal_id, str): if proposal_id in source_ids: issues.append( NegotiationVerificationIssue( "successor_proposal_id", "a refinement result must use a new proposal_id", f"{proposal_pointer}/proposal_id", ) ) if proposal_id in returned_proposal_ids: issues.append( NegotiationVerificationIssue( "duplicate_successor_proposal_id", "returned proposal_id values must be unique across the batch", f"{proposal_pointer}/proposal_id", ) ) returned_proposal_ids.add(proposal_id) if proposal.get("parent_proposal_id") != result.get("source_proposal_id"): issues.append( NegotiationVerificationIssue( "lineage", "parent_proposal_id must equal source_proposal_id", f"{proposal_pointer}/parent_proposal_id", ) ) terms = proposal.get("commercial_terms") claimed_digest = proposal.get("terms_digest") if not isinstance(terms, dict) or not isinstance(claimed_digest, str): issues.append( NegotiationVerificationIssue( "terms_digest", "proposal must carry commercial_terms and terms_digest", proposal_pointer, ) ) continue if wire_faithful: computed = compute_terms_digest(terms) computed_digests.append(computed) if not hmac.compare_digest(claimed_digest, computed): issues.append( NegotiationVerificationIssue( "terms_digest", "terms_digest does not match RFC 8785 commercial_terms digest", f"{proposal_pointer}/terms_digest", ) ) if outcome == "finalized": expires_at = _parse_datetime(proposal.get("expires_at")) if expires_at is None or expires_at <= verification_time: issues.append( NegotiationVerificationIssue( "proposal_expired", "a finalized hold must have expires_at later than verification time", f"{proposal_pointer}/expires_at", ) ) source = source_proposals.get(str(source_id)) if source_proposals else None source_data = _as_dict(source) if source is not None else None source_terms = source_data.get("commercial_terms") if source_data else None source_digest = source_data.get("terms_digest") if source_data else None source_status = source_data.get("proposal_status") if source_data else None if source_data is not None and source_data.get("proposal_id") != source_id: issues.append( NegotiationVerificationIssue( "source_proposal_identity", "source proposal payload does not match the requested proposal_id", proposal_pointer, ) ) if not isinstance(source_terms, dict): issues.append( NegotiationVerificationIssue( "source_proposal_required", "finalize verification requires the original source proposal", proposal_pointer, ) ) else: if source_status != "draft": issues.append( NegotiationVerificationIssue( "source_proposal_state", "finalize requires a draft source proposal", proposal_pointer, ) ) finalized_digest = compute_terms_digest(terms) if isinstance(source, BaseModel): # Parsed models normalize wire lexemes (notably RFC3339 # offsets), so their commercial_terms cannot be rehashed # faithfully. Compare the retained source digest to the # independently verified finalized wire digest instead. computed_source_digest = source_digest else: computed_source_digest = compute_terms_digest(source_terms) if isinstance(source_digest, str) and not hmac.compare_digest( source_digest, computed_source_digest ): issues.append( NegotiationVerificationIssue( "source_terms_digest", "source proposal terms_digest does not match its " "commercial_terms", f"{proposal_pointer}/commercial_terms", ) ) if not isinstance(computed_source_digest, str): issues.append( NegotiationVerificationIssue( "source_terms_digest", "source proposal must carry its original terms_digest", f"{proposal_pointer}/commercial_terms", ) ) elif not hmac.compare_digest(computed_source_digest, finalized_digest): issues.append( NegotiationVerificationIssue( "finalize_terms_changed", "finalize must preserve the source proposal's commercial_terms", f"{proposal_pointer}/commercial_terms", ) ) constraints = refinement.get("constraints") failed_constraints = _failed_constraints( constraints if isinstance(constraints, dict) else {}, terms ) failed_constraint_union.update(failed_constraints) declared_unsatisfied = { str(value) for value in result.get("unsatisfied_constraints", []) } undeclared = failed_constraints - declared_unsatisfied if undeclared: issues.append( NegotiationVerificationIssue( "constraint_mismatch", f"proposal fails undeclared constraints: {sorted(undeclared)!r}", proposal_pointer, ) ) changes = refinement.get("product_changes", {}) if isinstance(changes, dict): failed_changes = _failed_product_changes(changes, terms) failed_change_union.update(failed_changes) declared_changes = result.get("unsatisfied_product_changes", {}) declared_ids = ( set(declared_changes) if isinstance(declared_changes, dict) else set() ) undeclared_changes = failed_changes - declared_ids if undeclared_changes: issues.append( NegotiationVerificationIssue( "product_change_mismatch", "proposal fails undeclared product changes: " f"{sorted(undeclared_changes)!r}", proposal_pointer, ) ) declared_unsatisfied = {str(value) for value in result.get("unsatisfied_constraints", [])} if "unsatisfied_constraints" in result and not result.get("unsatisfied_constraints"): issues.append( NegotiationVerificationIssue( "constraint_mismatch", "unsatisfied_constraints must be non-empty when present", f"{pointer}/unsatisfied_constraints", ) ) requested_constraints = refinement.get("constraints", {}) invalid_declared_constraints = { key for key in declared_unsatisfied if not isinstance(requested_constraints, dict) or key not in requested_constraints } if invalid_declared_constraints: issues.append( NegotiationVerificationIssue( "constraint_mismatch", "unsatisfied_constraints must be a subset of requested constraint keys: " f"{sorted(invalid_declared_constraints)!r}", f"{pointer}/unsatisfied_constraints", ) ) if proposals and declared_unsatisfied - failed_constraint_union: issues.append( NegotiationVerificationIssue( "constraint_mismatch", "unsatisfied_constraints includes constraints satisfied by every proposal: " f"{sorted(declared_unsatisfied - failed_constraint_union)!r}", f"{pointer}/unsatisfied_constraints", ) ) declared_changes = result.get("unsatisfied_product_changes", {}) if "unsatisfied_product_changes" in result and not declared_changes: issues.append( NegotiationVerificationIssue( "product_change_mismatch", "unsatisfied_product_changes must be non-empty when present", f"{pointer}/unsatisfied_product_changes", ) ) requested_changes = refinement.get("product_changes", {}) declared_change_ids = set(declared_changes) if isinstance(declared_changes, dict) else set() if isinstance(declared_changes, dict): invalid_declared_changes = { product_id: action for product_id, action in declared_changes.items() if not isinstance(requested_changes, dict) or requested_changes.get(product_id) != action } if invalid_declared_changes: issues.append( NegotiationVerificationIssue( "product_change_mismatch", "unsatisfied_product_changes must be an exact subset of the " "requested product_changes map", f"{pointer}/unsatisfied_product_changes", ) ) if proposals and declared_change_ids - failed_change_union: issues.append( NegotiationVerificationIssue( "product_change_mismatch", "unsatisfied_product_changes includes changes satisfied by every proposal: " f"{sorted(declared_change_ids - failed_change_union)!r}", f"{pointer}/unsatisfied_product_changes", ) ) if len(computed_digests) != len(set(computed_digests)): issues.append( NegotiationVerificationIssue( "duplicate_alternative", "returned alternatives must have distinct commercial_terms", f"{pointer}/proposals", ) ) alternatives = refinement.get("alternatives") requested_count = alternatives.get("count") if isinstance(alternatives, dict) else None if reason_code == "alternatives_unavailable" and not ( isinstance(requested_count, int) and len(proposals) < requested_count ): issues.append( NegotiationVerificationIssue( "reason_mismatch", "alternatives_unavailable requires fewer proposals than requested", f"{pointer}/reason_code", ) ) if isinstance(requested_count, int) and result.get("outcome") == "revised": if len(proposals) != requested_count: issues.append( NegotiationVerificationIssue( "alternative_count", f"revised result must contain {requested_count} alternatives", f"{pointer}/proposals", ) ) elif isinstance(requested_count, int) and len(proposals) > requested_count: issues.append( NegotiationVerificationIssue( "alternative_count", f"result exceeds requested alternative count {requested_count}", f"{pointer}/proposals", ) ) elif requested_count is None and result.get("outcome") in {"revised", "partial"}: if len(proposals) != 1: issues.append( NegotiationVerificationIssue( "alternative_count", "a revision without alternatives must return exactly one proposal", f"{pointer}/proposals", ) ) unsatisfied = result.get("unsatisfied_constraints") unsatisfied_changes = result.get("unsatisfied_product_changes") if (unsatisfied or unsatisfied_changes) and result.get("outcome") not in { "partial", "unable", }: issues.append( NegotiationVerificationIssue( "unsatisfied_outcome", "unsatisfied constraints or product changes require partial or unable", f"{pointer}/outcome", ) ) if (unsatisfied or unsatisfied_changes) and result.get( "reason_code" ) != "constraint_unsatisfiable": issues.append( NegotiationVerificationIssue( "constraint_precedence", "unsatisfied typed constraints require reason_code constraint_unsatisfiable", f"{pointer}/reason_code", ) ) if reason_code == "constraint_unsatisfiable" and not (unsatisfied or unsatisfied_changes): issues.append( NegotiationVerificationIssue( "constraint_precedence", "reason_code constraint_unsatisfiable requires at least one " "unsatisfied constraint or product change", f"{pointer}/reason_code", ) ) if any( isinstance(result, dict) and result.get("reason_code") == "batch_aborted" for result in results ) and not any( isinstance(result, dict) and result.get("outcome") == "unable" and result.get("reason_code") not in {None, "batch_aborted"} for result in results ): issues.append( NegotiationVerificationIssue( "reason_mismatch", "batch_aborted requires a sibling finalize failure", "/results", ) ) return NegotiationVerificationResult(tuple(issues))Verify cross-object invariants for a completed refine response.
Submitted/working responses without
resultsare valid but have no proposal invariants to inspect. The helper never mutates either model. def verify_refinement_result(request: BaseModel | Mapping[str, Any],
task_result: TaskResult[Any],
*,
proposal_refinement: BaseModel | Mapping[str, Any] | None = None,
source_proposals: Mapping[str, BaseModel | Mapping[str, Any]] | None = None,
now: datetime | None = None) ‑> VerifiedRefinementResult-
Expand source code
def verify_refinement_result( request: BaseModel | Mapping[str, Any], task_result: TaskResult[Any], *, proposal_refinement: BaseModel | Mapping[str, Any] | None = None, source_proposals: Mapping[str, BaseModel | Mapping[str, Any]] | None = None, now: datetime | None = None, ) -> VerifiedRefinementResult: """Verify a terminal poll/webhook result without repeating transport.""" recovery = unsupported_refinement_recovery(task_result) verification: NegotiationVerificationResult | None = None if ( task_result.status == TaskStatus.COMPLETED and task_result.success and task_result.adcp_error is None ): response_data, _, _ = _response_data(task_result) try: _refine_response_adapter().validate_python(response_data) except ValidationError as exc: issues = tuple( NegotiationVerificationIssue( "invalid_response_schema", error["msg"], "/" + "/".join(str(part) for part in error["loc"]), ) for error in exc.errors(include_url=False) ) raise NegotiationVerificationError(issues) from exc verification = verify_refine_proposals_response( request, task_result, proposal_refinement=proposal_refinement, source_proposals=source_proposals, now=now, ) verification.raise_for_errors() return VerifiedRefinementResult(task_result, verification, recovery)Verify a terminal poll/webhook result without repeating transport.
def verify_terms_digest(proposal: Mapping[str, Any]) ‑> bool-
Expand source code
def verify_terms_digest(proposal: Mapping[str, Any]) -> bool: """Return whether a proposal's ``terms_digest`` matches its terms.""" if not isinstance(proposal, Mapping): raise TypeError("proposal must be the original wire mapping") wire_proposal = proposal terms = wire_proposal.get("commercial_terms") claimed = wire_proposal.get("terms_digest") if not isinstance(terms, dict) or not isinstance(claimed, str): return False return hmac.compare_digest(claimed, compute_terms_digest(terms))Return whether a proposal's
terms_digestmatches its terms.
Classes
class NegotiationVerificationError (issues: Sequence[NegotiationVerificationIssue])-
Expand source code
class NegotiationVerificationError(ValueError): """Raised when a refine-proposals response violates negotiation invariants.""" def __init__(self, issues: Sequence[NegotiationVerificationIssue]) -> None: self.issues = tuple(issues) summary = "; ".join(f"{issue.pointer}: {issue.message}" for issue in self.issues) super().__init__(summary)Raised when a refine-proposals response violates negotiation invariants.
Ancestors
- builtins.ValueError
- builtins.Exception
- builtins.BaseException
class NegotiationVerificationIssue (code: str, message: str, pointer: str)-
Expand source code
@dataclass(frozen=True, slots=True) class NegotiationVerificationIssue: """One failed proposal-negotiation invariant.""" code: str message: str pointer: strOne failed proposal-negotiation invariant.
Instance variables
var code : str-
Expand source code
@dataclass(frozen=True, slots=True) class NegotiationVerificationIssue: """One failed proposal-negotiation invariant.""" code: str message: str pointer: str var message : str-
Expand source code
@dataclass(frozen=True, slots=True) class NegotiationVerificationIssue: """One failed proposal-negotiation invariant.""" code: str message: str pointer: str var pointer : str-
Expand source code
@dataclass(frozen=True, slots=True) class NegotiationVerificationIssue: """One failed proposal-negotiation invariant.""" code: str message: str pointer: str
class NegotiationVerificationResult (issues: tuple[NegotiationVerificationIssue, ...] = ())-
Expand source code
@dataclass(frozen=True, slots=True) class NegotiationVerificationResult: """Aggregate result returned by :func:`verify_refine_proposals_response`.""" issues: tuple[NegotiationVerificationIssue, ...] = () @property def valid(self) -> bool: return not self.issues def raise_for_errors(self) -> None: """Raise :class:`NegotiationVerificationError` when verification failed.""" if self.issues: raise NegotiationVerificationError(self.issues)Aggregate result returned by :func:
verify_refine_proposals_response().Instance variables
var issues : tuple[NegotiationVerificationIssue, ...]-
Expand source code
@dataclass(frozen=True, slots=True) class NegotiationVerificationResult: """Aggregate result returned by :func:`verify_refine_proposals_response`.""" issues: tuple[NegotiationVerificationIssue, ...] = () @property def valid(self) -> bool: return not self.issues def raise_for_errors(self) -> None: """Raise :class:`NegotiationVerificationError` when verification failed.""" if self.issues: raise NegotiationVerificationError(self.issues) prop valid : bool-
Expand source code
@property def valid(self) -> bool: return not self.issues
Methods
def raise_for_errors(self) ‑> None-
Expand source code
def raise_for_errors(self) -> None: """Raise :class:`NegotiationVerificationError` when verification failed.""" if self.issues: raise NegotiationVerificationError(self.issues)Raise :class:
NegotiationVerificationErrorwhen verification failed.
class RefineProposalsClient (*args, **kwargs)-
Expand source code
class RefineProposalsClient(Protocol): """Minimal client surface consumed by :func:`refine_proposals_verified`.""" async def refine_proposals(self, request: Any) -> TaskResult[Any]: ...Minimal client surface consumed by :func:
refine_proposals_verified().Ancestors
- typing.Protocol
- typing.Generic
Methods
async def refine_proposals(self, request: Any) ‑> TaskResult[Any]-
Expand source code
async def refine_proposals(self, request: Any) -> TaskResult[Any]: ...
class UnsupportedRefinementRecovery (unsupported_dimension: str, supported_dimensions: tuple[str, ...])-
Expand source code
@dataclass(frozen=True, slots=True) class UnsupportedRefinementRecovery: """Typed recovery data from a task-level unsupported-dimension error.""" unsupported_dimension: str supported_dimensions: tuple[str, ...]Typed recovery data from a task-level unsupported-dimension error.
Instance variables
var supported_dimensions : tuple[str, ...]-
Expand source code
@dataclass(frozen=True, slots=True) class UnsupportedRefinementRecovery: """Typed recovery data from a task-level unsupported-dimension error.""" unsupported_dimension: str supported_dimensions: tuple[str, ...] var unsupported_dimension : str-
Expand source code
@dataclass(frozen=True, slots=True) class UnsupportedRefinementRecovery: """Typed recovery data from a task-level unsupported-dimension error.""" unsupported_dimension: str supported_dimensions: tuple[str, ...]
class VerifiedRefinementResult (task_result: TaskResult[Any],
verification: NegotiationVerificationResult | None = None,
unsupported_dimension: UnsupportedRefinementRecovery | None = None)-
Expand source code
@dataclass(frozen=True, slots=True) class VerifiedRefinementResult: """A transport result paired with completed-response verification.""" task_result: TaskResult[Any] verification: NegotiationVerificationResult | None = None unsupported_dimension: UnsupportedRefinementRecovery | None = None @property def valid(self) -> bool: """Return whether a completed transport result passed verification.""" return bool( self.task_result.success and self.task_result.status == TaskStatus.COMPLETED and self.verification is not None and self.verification.valid ) @property def pending(self) -> bool: """Return whether the seller accepted the request for async completion.""" return self.task_result.status in {TaskStatus.SUBMITTED, TaskStatus.WORKING}A transport result paired with completed-response verification.
Instance variables
prop pending : bool-
Expand source code
@property def pending(self) -> bool: """Return whether the seller accepted the request for async completion.""" return self.task_result.status in {TaskStatus.SUBMITTED, TaskStatus.WORKING}Return whether the seller accepted the request for async completion.
var task_result : TaskResult[Any]-
Expand source code
@dataclass(frozen=True, slots=True) class VerifiedRefinementResult: """A transport result paired with completed-response verification.""" task_result: TaskResult[Any] verification: NegotiationVerificationResult | None = None unsupported_dimension: UnsupportedRefinementRecovery | None = None @property def valid(self) -> bool: """Return whether a completed transport result passed verification.""" return bool( self.task_result.success and self.task_result.status == TaskStatus.COMPLETED and self.verification is not None and self.verification.valid ) @property def pending(self) -> bool: """Return whether the seller accepted the request for async completion.""" return self.task_result.status in {TaskStatus.SUBMITTED, TaskStatus.WORKING} var unsupported_dimension : UnsupportedRefinementRecovery | None-
Expand source code
@dataclass(frozen=True, slots=True) class VerifiedRefinementResult: """A transport result paired with completed-response verification.""" task_result: TaskResult[Any] verification: NegotiationVerificationResult | None = None unsupported_dimension: UnsupportedRefinementRecovery | None = None @property def valid(self) -> bool: """Return whether a completed transport result passed verification.""" return bool( self.task_result.success and self.task_result.status == TaskStatus.COMPLETED and self.verification is not None and self.verification.valid ) @property def pending(self) -> bool: """Return whether the seller accepted the request for async completion.""" return self.task_result.status in {TaskStatus.SUBMITTED, TaskStatus.WORKING} prop valid : bool-
Expand source code
@property def valid(self) -> bool: """Return whether a completed transport result passed verification.""" return bool( self.task_result.success and self.task_result.status == TaskStatus.COMPLETED and self.verification is not None and self.verification.valid )Return whether a completed transport result passed verification.
var verification : NegotiationVerificationResult | None-
Expand source code
@dataclass(frozen=True, slots=True) class VerifiedRefinementResult: """A transport result paired with completed-response verification.""" task_result: TaskResult[Any] verification: NegotiationVerificationResult | None = None unsupported_dimension: UnsupportedRefinementRecovery | None = None @property def valid(self) -> bool: """Return whether a completed transport result passed verification.""" return bool( self.task_result.success and self.task_result.status == TaskStatus.COMPLETED and self.verification is not None and self.verification.valid ) @property def pending(self) -> bool: """Return whether the seller accepted the request for async completion.""" return self.task_result.status in {TaskStatus.SUBMITTED, TaskStatus.WORKING}