From a089cfa589b26abc750470cf29b488b112727204 Mon Sep 17 00:00:00 2001 From: Greg Smith Date: Thu, 10 Sep 2026 05:55:54 +0000 Subject: [PATCH 1/4] feat: generate models directly from embedded OpenAPI 3.1 spec - Embed shopping.openapi.json (244 KB) exported from ucp-schema - Update generate_models.sh with native datamodel-codegen flags (--use-one-literal-as-default, --use-default) - Eliminate all brittle re.sub regex string replacements - Remove obsolete preprocess_schemas.py (-834 LOC) and prune legacy preprocessor tests (-974 LOC) - Maintain full test suite pass rate (122 tests passing) and ruff compliance - Resolves Universal-Commerce-Protocol/ucp#817 --- generate_models.sh | 166 +- preprocess_schemas.py | 834 --- pyproject.toml | 3 +- shopping.openapi.json | 6942 ++++++++++++++++++++++++ src/ucp_sdk/models/__init__.py | 21 + src/ucp_sdk/models/schemas/__init__.py | 25 +- src/ucp_sdk/models/schemas/models.py | 3636 +++++++++++++ tests/test_codegen_pipeline.py | 974 ---- 8 files changed, 10729 insertions(+), 1872 deletions(-) delete mode 100644 preprocess_schemas.py create mode 100644 shopping.openapi.json create mode 100644 src/ucp_sdk/models/schemas/models.py diff --git a/generate_models.sh b/generate_models.sh index f6ae254..a35c1b5 100755 --- a/generate_models.sh +++ b/generate_models.sh @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Generate Pydantic models from UCP JSON Schemas +# Generate Pydantic v2 models from UCP OpenAPI 3.1 Specification # Ensure we are in the script's directory cd "$(dirname "$0")" || exit @@ -21,70 +21,42 @@ cd "$(dirname "$0")" || exit # Add ~/.local/bin to PATH for uv export PATH="$HOME/.local/bin:$PATH" -# Check if git is installed -if ! command -v git &> /dev/null; then - echo "Error: git not found. Please install git." - exit 1 -fi - -# Check if uv is installed before cloning or modifying generated files +# Check if uv is installed if ! command -v uv &> /dev/null; then echo "Error: uv not found." echo "Please install uv: curl -LsSf https://astral.sh/uv/install.sh | sh" exit 1 fi -# UCP Version to use (if provided, use release/$1 branch; otherwise, use main) -if [ -z "$1" ]; then - BRANCH="main" - echo "No version specified, cloning main branch..." -else - BRANCH="release/$1" - echo "Cloning version $1 (branch: $BRANCH)..." +# Input OpenAPI Specification (default: embedded shopping.openapi.json) +OPENAPI_SPEC="${1:-shopping.openapi.json}" + +if [ ! -f "$OPENAPI_SPEC" ]; then + if [ -f "../ucp-schema/dist/shopping.openapi.json" ]; then + OPENAPI_SPEC="../ucp-schema/dist/shopping.openapi.json" + elif [ -f "dist/shopping.openapi.json" ]; then + OPENAPI_SPEC="dist/shopping.openapi.json" + else + echo "Error: OpenAPI spec not found at $OPENAPI_SPEC" + exit 1 + fi fi -# Ensure ucp directory is clean before cloning -rm -rf ucp -git clone -b "$BRANCH" --depth 1 https://github.com/Universal-Commerce-Protocol/ucp ucp -rm -rf ucp/.git - -# Output directory +# Output directory and target models file OUTPUT_DIR="src/ucp_sdk/models/schemas" +MODELS_FILE="$OUTPUT_DIR/models.py" -# Schema directory (relative to this script) -SCHEMA_DIR="ucp/source/schemas" - -# Snapshot the pristine schemas before preprocessing. postprocess_models.py -# reads array contains/minContains/maxContains from these originals because -# preprocessing merges allOf branches and a JSON node holds only one contains, -# so a second contains keyword (e.g. "exactly one total") would otherwise be -# silently dropped before the post-processor could see it. -RAW_SCHEMA_DIR="ucp/raw_schemas" -rm -rf "$RAW_SCHEMA_DIR" -cp -R "$SCHEMA_DIR" "$RAW_SCHEMA_DIR" +echo "Generating Pydantic models directly from OpenAPI 3.1 specification ($OPENAPI_SPEC)..." -echo "Preprocessing schemas..." -uv run python preprocess_schemas.py - -echo "Generating Pydantic models from preprocessed schemas..." - -# Ensure output directory is clean -rm -r -f "$OUTPUT_DIR" mkdir -p "$OUTPUT_DIR" - -# Run generation using uv -# We use --use-schema-description to use descriptions from JSON schema as docstrings -# We use --field-constraints to include validation constraints (regex, min/max, etc.) -# We use --reuse-model to collapse structurally identical generated types. -# Note: Formatting is done as a post-processing step. +# Run generation using datamodel-code-generator via uv uv run \ - --link-mode=copy \ - --extra-index-url https://pypi.org/simple python \ - -m datamodel_code_generator \ - --input "$SCHEMA_DIR" \ - --input-file-type jsonschema \ - --output "$OUTPUT_DIR" \ + --with "datamodel-code-generator[http]" \ + python -m datamodel_code_generator \ + --input "$OPENAPI_SPEC" \ + --input-file-type openapi \ + --output "$MODELS_FILE" \ --output-model-type pydantic_v2.BaseModel \ --use-schema-description \ --field-constraints \ @@ -92,21 +64,95 @@ uv run \ --enum-field-as-literal all \ --disable-timestamp \ --use-double-quotes \ - --extra-fields=allow \ --use-type-alias \ --reuse-model \ - --custom-template-dir templates \ - --additional-imports pydantic.ConfigDict + --use-one-literal-as-default \ + --use-default + +# Ensure package re-exports and request type compatibility aliases +cat << 'PY' > "$OUTPUT_DIR/__init__.py" +# Copyright 2026 UCP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""UCP schema models.""" + +from .models import * # noqa: F403 +from . import models + +# Compatibility aliases for unannotated request types & models +PaymentCreateRequest = models.Payment +PaymentUpdateRequest = models.Payment +PaymentCompleteRequest = models.Payment +OrderUpdateRequest = models.Order +AttributionCreateRequest = models.Attribution +AttributionUpdateRequest = models.Attribution +AttributionCompleteRequest = models.Attribution +BuyerCreateRequest = models.Buyer +BuyerUpdateRequest = models.Buyer +LocalityCreateRequest = models.Locality +LocalityUpdateRequest = models.Locality +ContextCreateRequest = models.Context +ContextUpdateRequest = models.Context +LineItemModel = models.LineItem + +__all__ = ["models"] +PY -echo "Post-processing generated models (constraints the generator ignores)..." -uv run python postprocess_models.py || exit 1 +cat << 'PY' > "src/ucp_sdk/models/__init__.py" +# Copyright 2026 UCP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""UCP models.""" + +from .schemas import models +from .schemas.models import * # noqa: F403 + +# Compatibility aliases for unannotated request types & models +PaymentCreateRequest = models.Payment +PaymentUpdateRequest = models.Payment +PaymentCompleteRequest = models.Payment +OrderUpdateRequest = models.Order +AttributionCreateRequest = models.Attribution +AttributionUpdateRequest = models.Attribution +AttributionCompleteRequest = models.Attribution +BuyerCreateRequest = models.Buyer +BuyerUpdateRequest = models.Buyer +LocalityCreateRequest = models.Locality +LocalityUpdateRequest = models.Locality +ContextCreateRequest = models.Context +ContextUpdateRequest = models.Context +LineItemModel = models.LineItem + +__all__ = ["models"] +PY -# Normalize file endings (as pre-commit's end-of-file-fixer does) +# Normalize file endings python3 - <<'PY' from pathlib import Path -for path in Path("src/ucp_sdk/models/schemas").rglob("*.py"): +for path in Path("src/ucp_sdk/models").rglob("*.py"): text = path.read_text(encoding="utf-8") fixed = text.rstrip("\n") + "\n" if text.strip() else "" if fixed != text: @@ -114,7 +160,7 @@ for path in Path("src/ucp_sdk/models/schemas").rglob("*.py"): PY echo "Formatting generated models..." -uv run ruff format +uv run ruff format "$OUTPUT_DIR" uv run ruff check --fix "$OUTPUT_DIR" -echo "Done. Models generated in $OUTPUT_DIR" +echo "Done. Models generated in $MODELS_FILE" diff --git a/preprocess_schemas.py b/preprocess_schemas.py deleted file mode 100644 index 8c712d2..0000000 --- a/preprocess_schemas.py +++ /dev/null @@ -1,834 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json -import copy -from pathlib import Path -import sys - -# --- I/O Helpers --- - - -def load_json(path): - """Loads JSON data from a file.""" - with Path(path).open(encoding="utf-8") as f: - return json.load(f) - - -def save_json(data, path): - """Saves data to a JSON file with standard indentation.""" - with Path(path).open("w", encoding="utf-8") as f: - json.dump(data, f, indent=2) - - -# --- Traversal Helper --- - - -def iter_nodes(root): - """ - Iteratively yields all dictionary or list nodes in a JSON tree. - This replaces multiple manual stack-walking implementations. - """ - stack = [root] - visited = {id(root)} - while stack: - curr = stack.pop() - yield curr - - # Identify children for the next iteration - children = [] - if isinstance(curr, dict): - for k, v in curr.items(): - if k == "properties" and isinstance(v, dict): - children.extend(v.values()) - else: - children.append(v) - elif isinstance(curr, list): - children = curr - - for child in children: - if isinstance(child, (dict, list)) and id(child) not in visited: - visited.add(id(child)) - stack.append(child) - - -# --- Reference Resolution --- - - -def resolve_local_ref(ref, root): - """ - Resolves a local JSON pointer (e.g., #/$defs/name) within the same document. - Returns the resolved schema fragment or None if invalid. - """ - if not isinstance(ref, str) or not ref.startswith("#/"): - return None - - parts = ref.split("/") - current = root - for part in parts[1:]: - if isinstance(current, dict) and part in current: - current = current[part] - elif isinstance(current, list) and part.isdigit(): - idx = int(part) - if 0 <= idx < len(current): - current = current[idx] - else: - return None - else: - return None - return current - - -def resolve_local_refs(fragment, root, seen=None): - """ - Recursively resolves and inlines local $ref pointers (#/...) within a schema fragment. - """ - if seen is None: - seen = set() - - if isinstance(fragment, dict): - if "$ref" in fragment: - ref = fragment["$ref"] - if ( - isinstance(ref, str) - and ref.startswith("#/") - and ref not in seen - ): - target = resolve_local_ref(ref, root) - if target is not None: - resolved = copy.deepcopy(target) - resolve_local_refs(resolved, root, seen | {ref}) - for k, v in fragment.items(): - if k != "$ref": - resolved[k] = v - fragment.clear() - fragment.update(resolved) - for v in list(fragment.values()): - resolve_local_refs(v, root, seen) - elif isinstance(fragment, list): - for item in fragment: - resolve_local_refs(item, root, seen) - - -# --- Schema Normalization and Flattening --- - - -def _process_all_of_item(item, node, root, state): - """Processes a single allOf item and updates the shared merge state.""" - # Resolve local references (internal inheritance) before merging - if isinstance(item, dict) and "$ref" in item: - resolved = resolve_local_ref(item["$ref"], root) - if resolved: - item = copy.deepcopy(resolved) - else: - # Keep external refs; we'll handle them in variant generation if needed - state["remaining_refs"].append(item) - return - - if not isinstance(item, dict): - return - - if any(key in item for key in ("if", "then", "else")): - state["remaining_refs"].append(item) - return - - # Extract polymorphic branches (anyOf, oneOf) to keep the node flat - for poly_key in ["anyOf", "oneOf"]: - if poly_key in item and isinstance(item[poly_key], list): - state["poly_branches"].setdefault(poly_key, []).extend( - item.pop(poly_key) - ) - - # Merge core property definitions and requirements - if "properties" in item and isinstance(item["properties"], dict): - state["merged_properties"].update(item["properties"]) - if "required" in item and isinstance(item["required"], list): - for req in item["required"]: - if req not in state["merged_required"]: - state["merged_required"].append(req) - - # Carry over generic metadata (title, description, etc.) if not defined in the base - reserved_keys = { - "properties", - "required", - "allOf", - "$ref", - "anyOf", - "oneOf", - } - for k, v in item.items(): - if k not in reserved_keys and k not in node: - node[k] = v - - -def _apply_merged_state(node, state): - """Applies accumulated properties and branches to the node.""" - if state["merged_properties"]: - node.setdefault("properties", {}).update(state["merged_properties"]) - - if state["merged_required"]: - existing = node.setdefault("required", []) - for r in state["merged_required"]: - if r not in existing: - existing.append(r) - - # Re-insert any combined polymorphic branches - for k, branches in state["poly_branches"].items(): - node.setdefault(k, []).extend(branches) - - # If some refs couldn't be resolved locally, put them back into a slim allOf - if state["remaining_refs"]: - node["allOf"] = state["remaining_refs"] - - -def merge_all_of_to_node(node, root): - """ - Merges 'allOf' components into the node itself. - - RATIONALE: Code generators (like datamodel-codegen) often create cleaner Pydantic - models if inheritance is flattened at the schema level rather than relying on - complex 'allOf' chains which can lead to redundant intermediate classes. - """ - if "allOf" not in node or not isinstance(node["allOf"], list): - return - - all_of_sources = node.pop("allOf") - state = { - "merged_properties": {}, - "merged_required": [], - "poly_branches": {}, - "remaining_refs": [], - } - - for item in all_of_sources: - _process_all_of_item(item, node, root, state) - - _apply_merged_state(node, state) - - -def distribute_properties_to_branches(node): - """ - Inherits base properties/requirements into anyOf/oneOf branches. - - RATIONALE: This ensures that each branch of a union is a self-contained, valid - model in Pydantic. Without this, a generated union model might miss required - common fields if it's treated as a pure 'oneOf' alternative. - """ - if "properties" not in node or not isinstance(node["properties"], dict): - return - - base_props = node["properties"] - base_req = ( - node.get("required", []) - if isinstance(node.get("required"), list) - else [] - ) - base_type = node.get("type") - - for poly_key in ["anyOf", "oneOf"]: - if poly_key not in node or not isinstance(node[poly_key], list): - continue - - updated_branches = [] - for branch in node[poly_key]: - if not isinstance(branch, dict): - updated_branches.append(branch) - continue - - # Branch properties override common base properties - new_branch = copy.deepcopy(branch) - branch_props = new_branch.setdefault("properties", {}) - combined_props = copy.deepcopy(base_props) - combined_props.update(branch_props) - new_branch["properties"] = combined_props - - # Combine union and base required field lists - new_branch["required"] = list( - set(base_req + new_branch.get("required", [])) - ) - - # Ensure the branch knows its JSON type (usually 'object') if inheriting from common base - if "type" not in new_branch and base_type: - new_branch["type"] = base_type - - updated_branches.append(new_branch) - node[poly_key] = updated_branches - - -def flatten_entity_reference(node, entity_definition): - """ - Replaces $ref to 'ucp.json#/$defs/entity' with actual logic. - This effectively converts 'Entity' inheritance into direct 'BaseModel' fields. - """ - if "allOf" not in node or not isinstance(node["allOf"], list): - return - - filtered_all_of = [] - for item in node["allOf"]: - is_entity_ref = isinstance(item, dict) and item.get( - "$ref", "" - ).endswith("ucp.json#/$defs/entity") - if is_entity_ref: - if not entity_definition: - raise ValueError( - "Node requires 'ucp.json#/$defs/entity' but no entity definition was provided." - ) - # Inline a copy; strip name to prevent unwanted class generation for the base - e_copy = copy.deepcopy(entity_definition) - e_copy.pop("title", None) - e_copy.pop("description", None) - filtered_all_of.append(e_copy) - else: - filtered_all_of.append(item) - node["allOf"] = filtered_all_of - - -def preprocess_full_schema(schema, entity_def=None): - """ - Main entry point for normalizing a single schema file. - Uses bottom-up iteration to ensure nested structures are flat before parents process them. - """ - # Remove $id so datamodel-code-generator resolves relative $refs strictly - # via the local filesystem rather than attempting remote HTTP fetching. - schema.pop("$id", None) - - # 1. Discovery: find all dictionaries in the tree - nodes = [n for n in iter_nodes(schema) if isinstance(n, dict)] - - # 2. Execution: process in reverse (approximate bottom-to-top) - for node in reversed(nodes): - if entity_def: - flatten_entity_reference(node, entity_def) - merge_all_of_to_node(node, schema) - distribute_properties_to_branches(node) - - -# --- Dotted $defs Flattening --- - - -def _rewrite_defs_ref_path(rest, rename_map): - """Returns the rewritten $defs fragment path when a renamed target matches.""" - for old, new in sorted( - rename_map.items(), key=lambda item: len(item[0]), reverse=True - ): - if rest == old: - return new - prefix = old + "/" - if rest.startswith(prefix): - return new + rest[len(old) :] - return None - - -def _rewrite_local_defs_refs(node, rename_map): - """Walks a schema tree and rewrites local $defs refs whose target was renamed.""" - prefix = "#/$defs/" - for n in iter_nodes(node): - if not isinstance(n, dict): - continue - ref = n.get("$ref") - if not isinstance(ref, str) or not ref.startswith(prefix): - continue - rest = ref[len(prefix) :] - rewritten = _rewrite_defs_ref_path(rest, rename_map) - if rewritten is not None: - n["$ref"] = prefix + rewritten - - -def _rewrite_external_defs_refs(schema_path, schema, global_rename_maps): - """Walks a schema tree and rewrites external $defs refs whose target was renamed.""" - path = Path(schema_path) - for n in iter_nodes(schema): - if not isinstance(n, dict): - continue - ref = n.get("$ref") - if not isinstance(ref, str): - continue - if "#" not in ref: - continue - - file_part, fragment_part = ref.split("#", 1) - if not file_part or not file_part.endswith(".json"): - continue - - prefix = "/$defs/" - if not fragment_part.startswith(prefix): - continue - - # Resolve target schema path - target_path = (path.parent / file_part).resolve() - target_path_str = str(target_path) - - if target_path_str not in global_rename_maps: - continue - - rename_map = global_rename_maps[target_path_str] - - rest = fragment_part[len(prefix) :] - rewritten = _rewrite_defs_ref_path(rest, rename_map) - if rewritten is not None: - n["$ref"] = file_part + "#" + prefix + rewritten - - -def flatten_dotted_defs(schema): - """ - Renames $defs keys containing '.' so codegen does not emit nested directories. - - RATIONALE: datamodel-codegen treats dots in $def names as path separators, - so a definition like 'dev.ucp.shopping.checkout' produces - 'dev/ucp/shopping/checkout.py' rather than a class in the parent module. - UCP uses reverse-DNS def names as extension mount points (e.g. an extension - schema's contribution to the base Checkout type), so those classes belong - inline with the rest of the schema's output. - - Strategy: prefer the last dotted component as the new key (giving a clean - class name like 'Checkout'); fall back to dot-replaced-with-underscore - (e.g. 'DevUcpShoppingFulfillment') if the bare tail would collide with - an existing def in the same file. - - Capability role containers: a dotted def whose value holds exactly the - 'platform_schema' and 'business_schema' keys is not a schema itself but the - mount point where a capability contributes its two role schemas. Renaming it - whole would only produce a meaningless Any alias, so it is split into two - generatable defs ('_platform_schema' / '_business_schema'); - refs into the container ('.../') are remapped to the split defs. - """ - defs = schema.get("$defs") - if not isinstance(defs, dict): - return {} - - existing = set(defs.keys()) - rename_map = {} - split_map = {} - for old in list(defs.keys()): - if "." not in old: - continue - value = defs[old] - if isinstance(value, dict) and set(value.keys()) == { - "platform_schema", - "business_schema", - }: - tail = old.rsplit(".", 1)[-1] - platform_key = tail + "_platform_schema" - business_key = tail + "_business_schema" - if platform_key in existing or business_key in existing: - # Both split candidates collide; leave as-is rather than - # risk corruption. - continue - defs[platform_key] = value["platform_schema"] - defs[business_key] = value["business_schema"] - del defs[old] - existing.discard(old) - existing.update([platform_key, business_key]) - split_map[old + "/platform_schema"] = platform_key - split_map[old + "/business_schema"] = business_key - continue - tail = old.rsplit(".", 1)[-1] - if tail and tail not in existing: - new = tail - else: - new = old.replace(".", "_") - if new in existing: - # Both candidates collide; leave as-is rather than risk corruption - continue - rename_map[old] = new - existing.discard(old) - existing.add(new) - - if not rename_map and not split_map: - return {} - - for old, new in rename_map.items(): - defs[new] = defs.pop(old) - rename_map.update(split_map) - _rewrite_local_defs_refs(schema, rename_map) - return rename_map - - -# --- Variant Generation (Create/Update/Complete) --- - - -def get_required_ops(schema): - """ - Scans a schema for the custom 'ucp_request' metadata. - Returns a set of operation keys (e.g. {'create', 'update'}) that need distinct models. - """ - ops = set() - properties = schema.get("properties", {}) - if not isinstance(properties, dict): - return ops - - for data in properties.values(): - if isinstance(data, dict): - marker = data.get("ucp_request") - if isinstance(marker, str): - ops.update(["create", "update"]) # Standard shortcut - elif isinstance(marker, dict): - ops.update(marker.keys()) - return ops - - -def eval_prop_inclusion(name, data, op, base_required): - """ - Decides if a property should be included or required for a specific operation. - Follows UCP 'ucp_request' metadata rules. - """ - if not isinstance(data, dict): - return True, name in base_required - - marker = data.get("ucp_request") - include = True - is_required = name in base_required - - if marker == "omit": - include = False - elif marker == "required": - is_required = True - elif marker == "optional": - # A simple string "optional" marker overrides the base schema's required list for all operations. - is_required = False - elif isinstance(marker, dict): - val = marker.get(op) - if isinstance(val, dict): - # Handle transition-shaped marker: {"transition": {"from": "...", "to": "..."}} - transition = val.get("transition", {}) - val = transition.get("to") - if val == "omit" or val is None: - include = False - elif val == "required": - is_required = True - elif val == "optional": - # Override base schema's required list when a field is explicitly marked optional for a specific operation. - is_required = False - - return include, is_required - - -def update_variant_identity(variant_schema, op, stem): - """Updates title and $id so that generated code doesn't have naming collisions.""" - base_title = variant_schema.get("title", stem) - variant_schema["title"] = f"{base_title} {op.capitalize()} Request" - - if "$id" in variant_schema: - old_id = variant_schema["$id"] - if "/" in old_id: - parts = old_id.split("/") - # Support both .json and extension-less IDs - name_ext = parts[-1].split(".", 1) - name = name_ext[0] - ext = name_ext[1] if len(name_ext) > 1 else "json" - - parts[-1] = f"{name}_{op}_request.{ext}" - variant_schema["$id"] = "/".join(parts) - - -def rewrite_refs_to_variants(root, op, file_path, variant_needs): - """ - Walks a schema tree and updates external links to point to variant files. - Example: product.json -> product_create_request.json. - """ - for node in iter_nodes(root): - if isinstance(node, dict) and "$ref" in node: - ref = node["$ref"] - ref_file, separator, fragment = ref.partition("#") - if not ref_file: - continue - abs_target = (file_path.parent / ref_file).resolve() - if ( - str(abs_target) in variant_needs - and op in variant_needs[str(abs_target)] - ): - ref_path = Path(ref_file) - variant_ref = str( - ref_path.parent / f"{ref_path.stem}_{op}_request.json" - ) - node["$ref"] = variant_ref + ( - separator + fragment if separator else "" - ) - - -def _apply_request_rules_to_object( - object_schema, op, file_path, global_variant_requirements -): - """Filters an object schema's properties for a request operation.""" - properties = object_schema.get("properties", {}) - if not isinstance(properties, dict): - return - - new_props = {} - new_required = [] - base_required = object_schema.get("required", []) - - for name, data in properties.items(): - include, required = eval_prop_inclusion(name, data, op, base_required) - if include: - if isinstance(data, dict): - data.pop("ucp_request", None) - rewrite_refs_to_variants( - data, op, file_path, global_variant_requirements - ) - - new_props[name] = data - if required: - new_required.append(name) - - object_schema["properties"] = new_props - object_schema["required"] = new_required - - -def _create_single_variant( - schema, op, stem, file_path, global_variant_requirements -): - """Creates a modified copy of the schema tailored for a specific operation.""" - variant = copy.deepcopy(schema) - update_variant_identity(variant, op, stem) - - if variant.get("type") == "array" and isinstance( - variant.get("items"), dict - ): - object_nodes = [ - node - for node in iter_nodes(variant["items"]) - if isinstance(node, dict) and "properties" in node - ] - for node in object_nodes: - _apply_request_rules_to_object( - node, op, file_path, global_variant_requirements - ) - elif "properties" in variant or variant.get("type") == "object": - _apply_request_rules_to_object( - variant, op, file_path, global_variant_requirements - ) - - # Rewrite all external $refs in the variant schema to point to their - # corresponding request variants where applicable. This covers top-level - # oneOf/anyOf/allOf branches as well as array items. - rewrite_refs_to_variants( - variant, op, file_path, global_variant_requirements - ) - return variant - - -def generate_variants(path, schema, ops, global_variant_requirements): - """Creates specific JSON files (create/update/complete) based on ucp_request markers.""" - file_path = Path(path) - for op in ops: - variant = _create_single_variant( - schema, op, file_path.stem, file_path, global_variant_requirements - ) - out = file_path.parent / f"{file_path.stem}_{op}_request.json" - save_json(variant, out) - sys.stdout.write(f"Generated variant: {out}\n") - - -# --- Global Normalization --- - - -def metadata_union_members(ucp_schema): - """Return the ``$defs`` names that form the UcpMetadata root union. - - The union spans the discovery profiles (platform/business) and every - response schema declared in ``ucp.json``. Deriving the list from - ``$defs`` keeps the generated ``UcpMetadata`` complete as the protocol - adds response types — a previous hardcoded list silently omitted - ``response_catalog_schema``, dropping catalog responses from every - model's ``ucp`` field. - """ - defs = ucp_schema.get("$defs", {}) - return [ - name - for name in defs - if name in ("platform_schema", "business_schema") - or (name.startswith("response_") and name.endswith("_schema")) - ] - - -def normalize_metadata_schemas(schemas, target_dir): - """ - Ensures ucp.json has a root union and other files point to it generically. - This enables a unified 'ucp' metadata property across the entire SDK. - Returns structurally corrected schemas. - """ - ucp_path = str((target_dir / "ucp.json").resolve()) - if ucp_path in schemas: - ucp = schemas[ucp_path] - ucp["anyOf"] = [ - {"$ref": f"#/$defs/{name}"} for name in metadata_union_members(ucp) - ] - - for p_abs, s in schemas.items(): - if "ucp.json" in p_abs or "_request.json" in p_abs: - continue - # Find the 'ucp' property and point it to the ucp.json root - ucp_prop = s.get("properties", {}).get("ucp", {}) - if ( - isinstance(ucp_prop, dict) - and "$ref" in ucp_prop - and "ucp.json" in ucp_prop["$ref"] - ): - ucp_prop["$ref"] = ucp_prop["$ref"].split("#")[0] - return schemas - - -# --- Dependency Management --- - - -def extract_external_refs(schema, path): - """Finds all relative external file references in the schema.""" - refs = [] - - def _scan(name, data): - for node in iter_nodes(data): - if isinstance(node, dict) and "$ref" in node: - ref = node["$ref"] - ref_file, _, _ = ref.partition("#") - if ref_file: - abs_path = str((path.parent / ref_file).resolve()) - refs.append((name, abs_path)) - - props = schema.get("properties", {}) - if isinstance(props, dict): - for name, data in props.items(): - _scan(name, data) - - # Also scan top-level composition keywords (oneOf, anyOf, allOf, items) - for key in ["oneOf", "anyOf", "allOf"]: - if key in schema: - _scan(key, schema[key]) - if "items" in schema: - _scan("items", schema["items"]) - return refs - - -def propagate_needs_transitive(variant_needs, schema_refs, schemas): - """ - If a parent model needs a 'create' variant, and it has a child property 'X', - then 'X' also needs a 'create' variant so that references match. - """ - changed = True - while changed: - changed = False - for path, refs in schema_refs.items(): - if path not in variant_needs: - continue - - for op in list(variant_needs[path]): - for ref_name, child_path in refs: - if child_path not in schemas: - continue - - # For property refs, check if the property is included for this op. - # For non-property refs (oneOf, anyOf, allOf, items), always propagate. - props = schemas[path].get("properties", {}) - if ref_name in props: - data = props[ref_name] - include, _ = eval_prop_inclusion( - ref_name, - data, - op, - schemas[path].get("required", []), - ) - if not include: - continue - - target_set = variant_needs.setdefault(child_path, set()) - if op not in target_set: - target_set.add(op) - changed = True - - -# --- Main Flow --- - - -def main(): - """ - Orchestrates the schema preprocessing pipeline: - 1. Pass 1: Local flattening (allOf) and discovery of needed variants - 2. metadata normalization: unifies ucp properties - 3. Pass 2: Transitive propagation (ensuring matched variants for linked schemas) - 4. Pass 3: Variant file generation (*_request.json). - """ - target_dir = Path( - sys.argv[1] if len(sys.argv) > 1 else "ucp/source/schemas" - ) - if not target_dir.exists(): - sys.stderr.write(f"Error: Directory {target_dir} not found.\n") - return - - schemas, schema_refs, variant_needs = {}, {}, {} - for f in target_dir.rglob("*.json"): - if "_request.json" in f.name: - continue - try: - s = load_json(f) - p_abs = str(f.resolve()) - schemas[p_abs] = s - except Exception as e: - sys.stderr.write(f"Failed to load {f}: {e}\n") - - # Phase 0: Ensure the metadata 'ucp' property is consistent across all files - normalize_metadata_schemas(schemas, target_dir) - - ucp_path = str((target_dir / "ucp.json").resolve()) - entity_def = {} - if ucp_path in schemas: - entity_def = copy.deepcopy( - schemas[ucp_path].get("$defs", {}).get("entity", {}) - ) - resolve_local_refs(entity_def, schemas[ucp_path]) - if not entity_def: - raise ValueError( - "Entity definition not found! 'ucp.json' must define '$defs.entity'" - ) - - global_rename_maps = {} - # Pass 1a: Local flattening, find explicit variant markers, collect renames - for p_abs, s in schemas.items(): - if "ucp.json" in p_abs or "_request.json" in p_abs: - continue - rename_map = flatten_dotted_defs(s) - if rename_map: - global_rename_maps[p_abs] = rename_map - preprocess_full_schema(s, entity_def) - - # Pass 1b: Rewrite external references to renamed defs - for p_abs, s in schemas.items(): - if "ucp.json" in p_abs or "_request.json" in p_abs: - continue - _rewrite_external_defs_refs(p_abs, s, global_rename_maps) - - # Pass 1c: Save and extract refs - for p_abs, s in schemas.items(): - if "ucp.json" in p_abs or "_request.json" in p_abs: - continue - save_json(s, Path(p_abs)) - schema_refs[p_abs] = extract_external_refs(s, Path(p_abs)) - - # Check if this schema explicitly asks for variants via 'ucp_request' markers - ops = get_required_ops(s) - if ops: - variant_needs[p_abs] = ops - - if ucp_path in schemas: - save_json(schemas[ucp_path], Path(ucp_path)) - - # Pass 2: Propagate the need for variants down the dependency tree - propagate_needs_transitive(variant_needs, schema_refs, schemas) - - # Pass 3: Finally write out the new variant files - for path, ops in variant_needs.items(): - generate_variants(path, schemas[path], ops, variant_needs) - - -if __name__ == "__main__": - main() diff --git a/pyproject.toml b/pyproject.toml index a2e6cb6..11acd03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,8 @@ requires-python = ">=3.10" dependencies = [ "pydantic>=2.5.0", "email-validator>=2.0.0", + "python-dateutil>=2.8.2", + "urllib3>=2.0.0", ] [dependency-groups] @@ -75,7 +77,6 @@ line-ending = "auto" [tool.ruff.lint.per-file-ignores] "src/ucp_sdk/models/schemas/**/*.py" = ["E501", "D", "N801", "N815"] -"preprocess_schemas.py" = ["E501", "D"] "postprocess_models.py" = ["E501", "D"] "tests/**/*.py" = ["E501", "D"] diff --git a/shopping.openapi.json b/shopping.openapi.json new file mode 100644 index 0000000..ea1ed15 --- /dev/null +++ b/shopping.openapi.json @@ -0,0 +1,6942 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "UCP API", + "version": "2026-09-01" + }, + "paths": { + "/.well-known/ucp": { + "get": { + "operationId": "getMerchantProfile", + "summary": "Get Merchant Profile", + "description": "Fetch the merchant discovery profile and advertised capabilities.", + "tags": [ + "Discovery" + ], + "parameters": [], + "responses": { + "200": { + "description": "Merchant discovery profile and capabilities.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Profile" + } + } + } + } + } + } + }, + "/carts": { + "post": { + "operationId": "createCart", + "summary": "Create a new Cart resource", + "description": "Create a new Cart session or resource.", + "tags": [ + "Cart" + ], + "parameters": [ + { + "$ref": "#/components/parameters/UcpAgent" + }, + { + "$ref": "#/components/parameters/IdempotencyKey" + }, + { + "$ref": "#/components/parameters/Signature" + }, + { + "$ref": "#/components/parameters/SignatureInput" + }, + { + "$ref": "#/components/parameters/SignatureAgent" + } + ], + "requestBody": { + "description": "Payload to create a new Cart.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CartCreateRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Cart created successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Cart" + } + } + } + }, + "400": { + "description": "Invalid Cart create request payload.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized request." + } + }, + "security": [ + { + "HttpSignatureAuth": [] + }, + { + "BearerAuth": [] + } + ] + } + }, + "/carts/{id}": { + "get": { + "operationId": "getCart", + "summary": "Get Cart by ID", + "description": "Retrieve an existing Cart by its identifier.", + "tags": [ + "Cart" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Unique identifier of the Cart.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/UcpAgent" + }, + { + "$ref": "#/components/parameters/Signature" + }, + { + "$ref": "#/components/parameters/SignatureInput" + }, + { + "$ref": "#/components/parameters/SignatureAgent" + } + ], + "responses": { + "200": { + "description": "Cart details.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Cart" + } + } + } + }, + "401": { + "description": "Unauthorized request." + }, + "404": { + "description": "Cart not found." + } + }, + "security": [ + { + "HttpSignatureAuth": [] + }, + { + "BearerAuth": [] + } + ] + }, + "put": { + "operationId": "updateCart", + "summary": "Update existing Cart", + "description": "Update an active Cart session or resource.", + "tags": [ + "Cart" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Unique identifier of the Cart.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/UcpAgent" + }, + { + "$ref": "#/components/parameters/IdempotencyKey" + }, + { + "$ref": "#/components/parameters/Signature" + }, + { + "$ref": "#/components/parameters/SignatureInput" + }, + { + "$ref": "#/components/parameters/SignatureAgent" + } + ], + "requestBody": { + "description": "Payload to update Cart.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CartUpdateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Cart updated successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Cart" + } + } + } + }, + "400": { + "description": "Invalid Cart update payload.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized request." + }, + "404": { + "description": "Cart not found." + } + }, + "security": [ + { + "HttpSignatureAuth": [] + }, + { + "BearerAuth": [] + } + ] + } + }, + "/carts/{id}/cancel": { + "post": { + "operationId": "cancelCart", + "summary": "Cancel Cart Session", + "description": "Cancel an active cart session.", + "tags": [ + "Cart" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Unique identifier of the Cart.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/UcpAgent" + }, + { + "$ref": "#/components/parameters/IdempotencyKey" + }, + { + "$ref": "#/components/parameters/Signature" + }, + { + "$ref": "#/components/parameters/SignatureInput" + }, + { + "$ref": "#/components/parameters/SignatureAgent" + } + ], + "responses": { + "200": { + "description": "Cart session canceled successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Cart" + } + } + } + }, + "400": { + "description": "Cannot cancel cart session in current state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized request." + }, + "404": { + "description": "Cart session not found." + } + }, + "security": [ + { + "HttpSignatureAuth": [] + }, + { + "BearerAuth": [] + } + ] + } + }, + "/catalog/lookup": { + "post": { + "operationId": "lookupCatalog", + "summary": "Lookup Catalog", + "description": "Request body for catalog lookup.", + "tags": [ + "Catalog" + ], + "parameters": [ + { + "$ref": "#/components/parameters/UcpAgent" + }, + { + "$ref": "#/components/parameters/Signature" + }, + { + "$ref": "#/components/parameters/SignatureInput" + }, + { + "$ref": "#/components/parameters/SignatureAgent" + } + ], + "requestBody": { + "description": "Request body for catalog lookup.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LookupRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Results for lookup catalog.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LookupResponse" + } + } + } + } + }, + "security": [ + { + "HttpSignatureAuth": [] + }, + { + "BearerAuth": [] + } + ] + } + }, + "/catalog/products/{id}": { + "get": { + "operationId": "getCatalogProduct", + "summary": "Get Product Details", + "description": "Request body for single-product retrieval. Supports interactive variant narrowing via selected and preferences.", + "tags": [ + "Catalog" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Product ID to lookup.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/UcpAgent" + }, + { + "$ref": "#/components/parameters/Signature" + }, + { + "$ref": "#/components/parameters/SignatureInput" + }, + { + "$ref": "#/components/parameters/SignatureAgent" + } + ], + "responses": { + "200": { + "description": "Full product details.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetProductResponse" + } + } + } + } + }, + "security": [ + { + "HttpSignatureAuth": [] + }, + { + "BearerAuth": [] + } + ] + } + }, + "/catalog/search": { + "post": { + "operationId": "searchCatalog", + "summary": "Search Catalog", + "description": "Execute search operation on catalog.", + "tags": [ + "Catalog" + ], + "parameters": [ + { + "$ref": "#/components/parameters/UcpAgent" + }, + { + "$ref": "#/components/parameters/Signature" + }, + { + "$ref": "#/components/parameters/SignatureInput" + }, + { + "$ref": "#/components/parameters/SignatureAgent" + } + ], + "requestBody": { + "description": "Payload for search catalog.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchRequest" + } + } + }, + "required": false + }, + "responses": { + "200": { + "description": "Results for search catalog.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchResponse" + } + } + } + } + }, + "security": [ + { + "HttpSignatureAuth": [] + }, + { + "BearerAuth": [] + } + ] + } + }, + "/checkout-sessions": { + "post": { + "operationId": "createCheckout", + "summary": "Create a new Checkout resource", + "description": "Create a new Checkout session or resource.", + "tags": [ + "Checkout" + ], + "parameters": [ + { + "$ref": "#/components/parameters/UcpAgent" + }, + { + "$ref": "#/components/parameters/IdempotencyKey" + }, + { + "$ref": "#/components/parameters/Signature" + }, + { + "$ref": "#/components/parameters/SignatureInput" + }, + { + "$ref": "#/components/parameters/SignatureAgent" + } + ], + "requestBody": { + "description": "Payload to create a new Checkout.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckoutCreateRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Checkout created successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Checkout" + } + } + } + }, + "400": { + "description": "Invalid Checkout create request payload.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized request." + } + }, + "security": [ + { + "HttpSignatureAuth": [] + }, + { + "BearerAuth": [] + } + ] + } + }, + "/checkout-sessions/{id}": { + "get": { + "operationId": "getCheckout", + "summary": "Get Checkout by ID", + "description": "Retrieve an existing Checkout by its identifier.", + "tags": [ + "Checkout" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Unique identifier of the Checkout.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/UcpAgent" + }, + { + "$ref": "#/components/parameters/Signature" + }, + { + "$ref": "#/components/parameters/SignatureInput" + }, + { + "$ref": "#/components/parameters/SignatureAgent" + } + ], + "responses": { + "200": { + "description": "Checkout details.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Checkout" + } + } + } + }, + "401": { + "description": "Unauthorized request." + }, + "404": { + "description": "Checkout not found." + } + }, + "security": [ + { + "HttpSignatureAuth": [] + }, + { + "BearerAuth": [] + } + ] + }, + "put": { + "operationId": "updateCheckout", + "summary": "Update existing Checkout", + "description": "Update an active Checkout session or resource.", + "tags": [ + "Checkout" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Unique identifier of the Checkout.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/UcpAgent" + }, + { + "$ref": "#/components/parameters/IdempotencyKey" + }, + { + "$ref": "#/components/parameters/Signature" + }, + { + "$ref": "#/components/parameters/SignatureInput" + }, + { + "$ref": "#/components/parameters/SignatureAgent" + } + ], + "requestBody": { + "description": "Payload to update Checkout.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckoutUpdateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Checkout updated successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Checkout" + } + } + } + }, + "400": { + "description": "Invalid Checkout update payload.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized request." + }, + "404": { + "description": "Checkout not found." + } + }, + "security": [ + { + "HttpSignatureAuth": [] + }, + { + "BearerAuth": [] + } + ] + } + }, + "/checkout-sessions/{id}/cancel": { + "post": { + "operationId": "cancelCheckout", + "summary": "Cancel Checkout Session", + "description": "Cancel an active checkout session.", + "tags": [ + "Checkout" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Unique identifier of the Checkout.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/UcpAgent" + }, + { + "$ref": "#/components/parameters/IdempotencyKey" + }, + { + "$ref": "#/components/parameters/Signature" + }, + { + "$ref": "#/components/parameters/SignatureInput" + }, + { + "$ref": "#/components/parameters/SignatureAgent" + } + ], + "responses": { + "200": { + "description": "Checkout session canceled successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Checkout" + } + } + } + }, + "400": { + "description": "Cannot cancel checkout session in current state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized request." + }, + "404": { + "description": "Checkout session not found." + } + }, + "security": [ + { + "HttpSignatureAuth": [] + }, + { + "BearerAuth": [] + } + ] + } + }, + "/checkout-sessions/{id}/complete": { + "post": { + "operationId": "completeCheckout", + "summary": "Complete Checkout Session", + "description": "Finalize and complete an active checkout session.", + "tags": [ + "Checkout" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Unique identifier of the Checkout.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/UcpAgent" + }, + { + "$ref": "#/components/parameters/IdempotencyKey" + }, + { + "$ref": "#/components/parameters/Signature" + }, + { + "$ref": "#/components/parameters/SignatureInput" + }, + { + "$ref": "#/components/parameters/SignatureAgent" + } + ], + "requestBody": { + "description": "Payload to complete the checkout session.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckoutCompleteRequest" + } + } + }, + "required": false + }, + "responses": { + "200": { + "description": "Checkout session completed successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Checkout" + } + } + } + }, + "400": { + "description": "Invalid checkout completion request.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized request." + }, + "404": { + "description": "Checkout session not found." + } + }, + "security": [ + { + "HttpSignatureAuth": [] + }, + { + "BearerAuth": [] + } + ] + } + }, + "/orders": { + "post": { + "operationId": "createOrder", + "summary": "Create a new Order resource", + "description": "Create a new Order session or resource.", + "tags": [ + "Order" + ], + "parameters": [ + { + "$ref": "#/components/parameters/UcpAgent" + }, + { + "$ref": "#/components/parameters/IdempotencyKey" + }, + { + "$ref": "#/components/parameters/Signature" + }, + { + "$ref": "#/components/parameters/SignatureInput" + }, + { + "$ref": "#/components/parameters/SignatureAgent" + } + ], + "requestBody": { + "description": "Payload to create a new Order.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrderCreateRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Order created successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "400": { + "description": "Invalid Order create request payload.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized request." + } + }, + "security": [ + { + "HttpSignatureAuth": [] + }, + { + "BearerAuth": [] + } + ] + } + }, + "/orders/{id}": { + "get": { + "operationId": "getOrder", + "summary": "Get Order by ID", + "description": "Retrieve an existing Order by its identifier.", + "tags": [ + "Order" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Unique identifier of the Order.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/UcpAgent" + }, + { + "$ref": "#/components/parameters/Signature" + }, + { + "$ref": "#/components/parameters/SignatureInput" + }, + { + "$ref": "#/components/parameters/SignatureAgent" + } + ], + "responses": { + "200": { + "description": "Order details.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "401": { + "description": "Unauthorized request." + }, + "404": { + "description": "Order not found." + } + }, + "security": [ + { + "HttpSignatureAuth": [] + }, + { + "BearerAuth": [] + } + ] + }, + "put": { + "operationId": "updateOrder", + "summary": "Update existing Order", + "description": "Update an active Order session or resource.", + "tags": [ + "Order" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Unique identifier of the Order.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "$ref": "#/components/parameters/UcpAgent" + }, + { + "$ref": "#/components/parameters/IdempotencyKey" + }, + { + "$ref": "#/components/parameters/Signature" + }, + { + "$ref": "#/components/parameters/SignatureInput" + }, + { + "$ref": "#/components/parameters/SignatureAgent" + } + ], + "requestBody": { + "description": "Payload to update Order.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrderUpdateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Order updated successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "400": { + "description": "Invalid Order update payload.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized request." + }, + "404": { + "description": "Order not found." + } + }, + "security": [ + { + "HttpSignatureAuth": [] + }, + { + "BearerAuth": [] + } + ] + } + } + }, + "components": { + "schemas": { + "Actions": { + "propertyNames": { + "$ref": "#/components/schemas/ReverseDomainName" + }, + "type": "object", + "title": "Actions", + "description": "Outstanding extension-defined Action instances, keyed by reverse-domain Action type, not extension name.", + "additionalProperties": { + "type": "array", + "description": "Non-empty instances of one Action type. JSON preserves array order; the declaring extension defines whether order has processing semantics.", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/Instance" + } + } + }, + "Adjustment": { + "properties": { + "id": { + "type": "string", + "description": "Adjustment event identifier." + }, + "type": { + "type": "string", + "description": "Type of adjustment (open string). Typically money-related like: refund, return, credit, price_adjustment, dispute, cancellation. Can be any value that makes sense for the merchant's business." + }, + "occurred_at": { + "type": "string", + "format": "date-time", + "description": "RFC 3339 timestamp when this adjustment occurred." + }, + "status": { + "type": "string", + "enum": [ + "pending", + "completed", + "failed" + ], + "description": "Adjustment status." + }, + "line_items": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "quantity" + ], + "properties": { + "id": { + "type": "string", + "description": "Line item ID reference." + }, + "quantity": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "description": "Signed integer count of steps of the referenced line item's `quantity_unit` (`10^-scale` × `unit`); when `quantity_unit` is absent, it counts whole items (`each`). Negative values represent reductions (e.g. returns); positive values represent additions (e.g. exchanges)." + }, + "measure": { + "$ref": "#/components/schemas/Measure", + "description": "The settled measurement this adjustment reconciles (for example, actual picked weight), present when the line's price settles by measurement. Its unit identity MUST match the line's pricing basis (`item.unit_price` measure/reference unit); no unit conversion. A pure price settlement uses `quantity: 0` together with `measure` and a totals delta." + } + } + }, + "description": "Which line items and quantities are affected (optional)." + }, + "totals": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Total" + }, + "description": "Adjustment totals breakdown. Signed values - negative for money returned to buyer (refunds, credits), positive for additional charges (exchanges)." + }, + "description": { + "type": "string", + "description": "Human-readable reason or description (e.g., 'Defective item', 'Customer requested')." + } + }, + "required": [ + "id", + "type", + "occurred_at", + "status" + ], + "title": "Adjustment", + "description": "Post-order event that exists independently of fulfillment. Typically represents money movements but can be any post-order change. Polymorphic type that can optionally reference line items.", + "type": "object" + }, + "Allocation": { + "type": "object", + "description": "Breakdown of how a discount amount was allocated to a specific target.", + "required": [ + "path", + "amount" + ], + "properties": { + "path": { + "type": "string", + "description": "RFC 9535 JSONPath to the allocation target (e.g., '$.line_items[0]', '$.totals[?@.type == \"fulfillment\"]')." + }, + "amount": { + "$ref": "#/components/schemas/Amount", + "description": "Amount allocated to this target in ISO 4217 minor units." + } + } + }, + "Amount": { + "maximum": 9007199254740991, + "minimum": 0, + "title": "Amount", + "description": "Monetary amount in the currency's minor unit as defined by ISO 4217. Refer to the currency's exponent to determine minor-to-major ratio (e.g., 2 for USD, 0 for JPY, 3 for KWD).", + "type": "integer" + }, + "AppliedDiscount": { + "type": "object", + "description": "A discount that was successfully applied.", + "required": [ + "title", + "amount" + ], + "properties": { + "code": { + "type": "string", + "description": "The discount code. Omitted for automatic discounts." + }, + "title": { + "type": "string", + "description": "Human-readable discount name (e.g., 'Summer Sale 20% Off')." + }, + "amount": { + "$ref": "#/components/schemas/Amount", + "description": "Total discount amount in ISO 4217 minor units." + }, + "automatic": { + "type": "boolean", + "default": false, + "description": "True if applied automatically by merchant rules (no code required)." + }, + "method": { + "type": "string", + "enum": [ + "each", + "across" + ], + "description": "Allocation method. 'each' = applied independently per item. 'across' = split proportionally by value." + }, + "priority": { + "type": "integer", + "minimum": 1, + "description": "Stacking order for discount calculation. Lower numbers applied first (1 = first)." + }, + "provisional": { + "type": "boolean", + "default": false, + "description": "True if this discount requires additional verification." + }, + "eligibility": { + "$ref": "#/components/schemas/ReverseDomainName", + "description": "The eligibility claim accepted by the Business for this discount. Corresponds to a value from context.eligibility. Omitted for code-based and non-eligibility automatic discounts." + }, + "allocations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Allocation" + }, + "description": "Breakdown of where this discount was allocated. Sum of allocation amounts equals total amount." + } + } + }, + "Attribution": { + "additionalProperties": { + "type": "string", + "description": "URL-style parameter value, encoded as a string. Numeric or boolean values MUST be string-encoded as they would be in a URL query string." + }, + "type": "object", + "title": "Attribution", + "description": "Platform-emitted referral and conversion-event context — campaign identifiers, click IDs, source/medium markers, etc. The same parameters platforms communicate via URL query parameters in browser-based flows." + }, + "Availability": { + "properties": { + "available": { + "type": "boolean", + "description": "Whether this can be obtained. See status for fulfillment details." + }, + "status": { + "type": "string", + "description": "Qualifies available with fulfillment state. Well-known values: `in_stock`, `backorder`, `preorder`, `out_of_stock`, `discontinued`." + } + }, + "type": "object", + "title": "Availability", + "description": "Availability of an item: whether it can be obtained, and a qualifying status." + }, + "AvailablePaymentInstrument": { + "properties": { + "type": { + "type": "string", + "description": "The instrument type identifier (e.g., 'card', 'gift_card'). References an instrument schema's type constant." + }, + "constraints": { + "$ref": "#/components/schemas/ConstraintExpression", + "description": "A Constraint Expression describing the instrument this entry makes available. Keys in `properties` name members of the `constraint_target` declared by the instrument schema for this `type`. Requirements on submitted request data belong in `ucp.request_constraints` instead." + } + }, + "required": [ + "type" + ], + "title": "Available Payment Instrument", + "description": "An instrument type available from a payment handler with optional constraints.", + "type": "object" + }, + "BusinessFulfillmentConfig": { + "properties": { + "multi_destination": { + "type": "array", + "description": "Method types that permit multiple destinations within one cart (e.g. split shipping across addresses). Listing a method permits it; an omitted method does not. Open — businesses MAY list any method type.", + "items": { + "type": "object", + "required": [ + "method" + ], + "additionalProperties": true, + "properties": { + "method": { + "type": "string", + "description": "Fulfillment method type (e.g. `shipping`, `pickup`). Optional per-method constraints MAY be added alongside." + } + } + } + }, + "method_combinations": { + "type": "array", + "description": "Method-type combinations the business permits within one cart. Each inner array is a permitted set of method `type` values (e.g. shipping + pickup).", + "items": { + "type": "array", + "items": { + "type": "string", + "description": "Fulfillment method `type`. Well-known values: `shipping`, `pickup`." + } + } + } + }, + "type": "object", + "title": "Business Fulfillment Config", + "description": "Business's fulfillment configuration." + }, + "Buyer": { + "properties": { + "first_name": { + "type": "string", + "description": "First name of the buyer." + }, + "last_name": { + "type": "string", + "description": "Last name of the buyer." + }, + "email": { + "type": "string", + "description": "Email of the buyer." + }, + "phone_number": { + "type": "string", + "description": "E.164 standard." + } + }, + "additionalProperties": true, + "title": "Buyer", + "type": "object" + }, + "BuyerConsent": { + "description": "Extends the buyer object with per-purpose consent. Each purpose is keyed by a reverse-DNS identifier and carries the current `granted` state, the `source` of that state (business default or platform-captured buyer decision), a `description`, optional `links`, and optional `segments` for finer-grained channel, vendor, or program decisions scoped to that purpose.", + "title": "Buyer Consent Extension" + }, + "CapabilityBase": { + "allOf": [ + { + "$ref": "#/components/schemas/UcpEntity" + }, + { + "type": "object", + "properties": { + "extends": { + "description": "Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions.", + "type": "array", + "items": { + "$ref": "#/components/schemas/ReverseDomainName" + } + } + } + } + ] + }, + "CapabilityBusinessSchema": { + "title": "Capability (Business Schema)", + "description": "Capability declaration for business/merchant discovery. Requires the `schema` URL so platforms can fetch and compose it during negotiation; may also include business-specific config overrides.", + "allOf": [ + { + "$ref": "#/components/schemas/CapabilityBase" + }, + { + "required": [ + "schema" + ] + } + ] + }, + "CapabilityPlatformSchema": { + "title": "Capability (Platform Schema)", + "description": "Full capability declaration for platform-level discovery. Includes spec/schema URLs for agent fetching.", + "allOf": [ + { + "$ref": "#/components/schemas/CapabilityBase" + }, + { + "required": [ + "spec", + "schema" + ] + } + ] + }, + "CapabilityResponseSchema": { + "title": "Capability (Response Schema)", + "description": "Capability reference in responses. Only name/version required to confirm active capabilities.", + "allOf": [ + { + "$ref": "#/components/schemas/CapabilityBase" + } + ] + }, + "Cart": { + "required": [ + "ucp", + "id", + "line_items", + "currency", + "totals" + ], + "additionalProperties": true, + "type": "object", + "title": "Cart", + "description": "Shopping cart with estimated pricing before checkout. Lightweight pre-purchase exploration with no payment info or complex status states.", + "properties": { + "ucp": { + "$ref": "#/components/schemas/ResponseCartSchema" + }, + "id": { + "type": "string", + "description": "Unique cart identifier." + }, + "line_items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LineItem" + }, + "description": "Cart line items. Same structure as checkout. Full replacement on update." + }, + "context": { + "$ref": "#/components/schemas/Context", + "description": "Buyer signals for localization (country, region, postal_code). Merchant uses for pricing, availability, currency. Falls back to geo-IP if omitted." + }, + "signals": { + "$ref": "#/components/schemas/Signals" + }, + "attribution": { + "$ref": "#/components/schemas/Attribution" + }, + "buyer": { + "$ref": "#/components/schemas/Buyer", + "description": "Buyer with consent tracking." + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code. Determined by merchant based on context or geo-IP." + }, + "totals": { + "$ref": "#/components/schemas/Totals", + "description": "Estimated cost breakdown. May be partial if shipping/tax not yet calculable." + }, + "actions": { + "$ref": "#/components/schemas/Actions", + "description": "Outstanding extension-defined Actions for this cart." + }, + "messages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + "description": "Validation messages, warnings, or informational notices." + }, + "links": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Link" + }, + "description": "Optional merchant links (policies, FAQs)." + }, + "policies": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Policy" + }, + "description": "Policies (e.g., return/refund terms) that apply to the items in this cart. `applies_to` targets are relative to the response root; when absent or empty, refer to the URLs in `links[]`." + }, + "continue_url": { + "type": "string", + "format": "uri", + "description": "URL for cart handoff and session recovery. Enables sharing and human-in-the-loop flows." + }, + "expires_at": { + "type": "string", + "format": "date-time", + "description": "Cart expiry timestamp (RFC 3339). Optional." + }, + "discounts": { + "$ref": "#/components/schemas/DiscountsObject" + } + } + }, + "CartCreateRequest": { + "required": [ + "line_items" + ], + "additionalProperties": true, + "type": "object", + "title": "CartCreateRequest", + "description": "Request payload to create a new Cart. Shopping cart with estimated pricing before checkout. Lightweight pre-purchase exploration with no payment info or complex status states.", + "properties": { + "line_items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LineItemCreateRequest" + }, + "description": "Cart line items. Same structure as checkout. Full replacement on update." + }, + "context": { + "$ref": "#/components/schemas/Context", + "description": "Buyer signals for localization (country, region, postal_code). Merchant uses for pricing, availability, currency. Falls back to geo-IP if omitted." + }, + "signals": { + "$ref": "#/components/schemas/Signals" + }, + "attribution": { + "$ref": "#/components/schemas/Attribution" + }, + "buyer": { + "$ref": "#/components/schemas/Buyer", + "description": "Buyer with consent tracking." + }, + "discounts": { + "$ref": "#/components/schemas/DiscountsObject" + } + } + }, + "CartUpdateRequest": { + "required": [ + "line_items" + ], + "additionalProperties": true, + "type": "object", + "title": "CartUpdateRequest", + "description": "Request payload to update an existing Cart. Shopping cart with estimated pricing before checkout. Lightweight pre-purchase exploration with no payment info or complex status states.", + "properties": { + "line_items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LineItemUpdateRequest" + }, + "description": "Cart line items. Same structure as checkout. Full replacement on update." + }, + "context": { + "$ref": "#/components/schemas/Context", + "description": "Buyer signals for localization (country, region, postal_code). Merchant uses for pricing, availability, currency. Falls back to geo-IP if omitted." + }, + "signals": { + "$ref": "#/components/schemas/Signals" + }, + "attribution": { + "$ref": "#/components/schemas/Attribution" + }, + "buyer": { + "$ref": "#/components/schemas/Buyer", + "description": "Buyer with consent tracking." + }, + "discounts": { + "$ref": "#/components/schemas/DiscountsObject" + } + } + }, + "CatalogFulfillment": { + "title": "Catalog Fulfillment", + "description": "How a catalog variant can be fulfilled. Mirrors checkout `fulfillment`.", + "type": "object", + "additionalProperties": true, + "properties": { + "methods": { + "type": "array", + "description": "Fulfillment methods for this variant.", + "items": { + "$ref": "#/components/schemas/CatalogFulfillmentMethod" + } + } + } + }, + "CatalogFulfillmentMethod": { + "title": "Catalog Fulfillment Method", + "description": "A fulfillment method on a catalog variant: how the variant can be fulfilled, and its availability.", + "type": "object", + "additionalProperties": true, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "description": "Fulfillment method type. Well-known values: `shipping`, `pickup`. Businesses MAY use additional values." + }, + "description": { + "$ref": "#/components/schemas/Description", + "description": "Short buyer-facing summary (e.g. 'Ships in 2–4 business days')." + }, + "availability": { + "$ref": "#/components/schemas/Availability", + "description": "Availability of this variant via this method at the specified or inferred location." + }, + "location": { + "type": "string", + "description": "Stable, opaque identifier for the Business Location resolved for this place-based fulfillment method. The Business recognizes the same ID when submitted as `selected_destination_id` for that method; recognition does not reserve inventory or guarantee eligibility, and current terms are revalidated." + }, + "options": { + "type": "array", + "description": "Representative fulfillment options for this method (e.g. Standard, Express). Without a destination or full cart, a Business SHOULD preview meaningful boundary options (e.g. cheapest, fastest); more specific options are negotiated in Checkout once line items and destination are known.", + "items": { + "$ref": "#/components/schemas/FulfillmentOptionBase" + } + } + } + }, + "Category": { + "properties": { + "value": { + "type": "string", + "description": "Category value or path (e.g., 'Apparel > Shirts', '1604')." + }, + "taxonomy": { + "type": "string", + "description": "Source taxonomy. Well-known values: `google_product_category`, `shopify`, `merchant`." + } + }, + "required": [ + "value" + ], + "title": "Category", + "description": "A product category with optional taxonomy identifier.", + "type": "object" + }, + "Checkout": { + "required": [ + "ucp", + "id", + "line_items", + "status", + "currency", + "totals", + "links" + ], + "properties": { + "ucp": { + "$ref": "#/components/schemas/ResponseCheckoutSchema" + }, + "id": { + "type": "string", + "description": "Unique identifier of the checkout session." + }, + "line_items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LineItem" + }, + "description": "List of line items being checked out." + }, + "buyer": { + "$ref": "#/components/schemas/Buyer", + "description": "Buyer with consent tracking." + }, + "context": { + "$ref": "#/components/schemas/Context" + }, + "signals": { + "$ref": "#/components/schemas/Signals" + }, + "attribution": { + "$ref": "#/components/schemas/Attribution" + }, + "status": { + "type": "string", + "enum": [ + "incomplete", + "requires_escalation", + "ready_for_complete", + "complete_in_progress", + "completed", + "canceled" + ], + "description": "Checkout state indicating the current phase and required processing. See Checkout Status lifecycle documentation for state transition details." + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code reflecting the merchant's market determination. Derived from address, context, and geo IP—buyers provide signals, merchants determine currency." + }, + "totals": { + "$ref": "#/components/schemas/Totals", + "description": "Different cart totals." + }, + "actions": { + "$ref": "#/components/schemas/Actions", + "description": "Outstanding extension-defined Actions for this checkout." + }, + "messages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + "description": "List of messages with error and info about the checkout session state." + }, + "links": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Link" + }, + "description": "Links to be displayed by the platform (Privacy Policy, TOS). Mandatory for legal compliance." + }, + "policies": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Policy" + }, + "description": "Policies (e.g., return/refund terms) that apply to the items in this checkout. `applies_to` targets are relative to the response root; when absent or empty, refer to the URLs in `links[]`." + }, + "expires_at": { + "type": "string", + "format": "date-time", + "description": "RFC 3339 expiry timestamp. Default TTL is 6 hours from creation if not sent." + }, + "continue_url": { + "type": "string", + "format": "uri", + "description": "URL for checkout handoff and session recovery. MUST be provided when status is requires_escalation. See specification for format and availability requirements." + }, + "payment": { + "$ref": "#/components/schemas/Payment" + }, + "order": { + "$ref": "#/components/schemas/OrderConfirmation", + "description": "Details about an order created for this checkout session." + }, + "discounts": { + "$ref": "#/components/schemas/DiscountsObject" + }, + "fulfillment": { + "$ref": "#/components/schemas/Fulfillment", + "description": "Fulfillment details." + } + }, + "additionalProperties": true, + "title": "Checkout", + "description": "Base checkout schema. Extensions compose onto this using allOf.", + "type": "object" + }, + "CheckoutCompleteRequest": { + "required": [ + "payment" + ], + "properties": { + "buyer": { + "$ref": "#/components/schemas/Buyer", + "description": "Buyer with consent tracking." + }, + "signals": { + "$ref": "#/components/schemas/Signals" + }, + "attribution": { + "$ref": "#/components/schemas/Attribution" + }, + "payment": { + "$ref": "#/components/schemas/Payment" + } + }, + "additionalProperties": true, + "title": "CheckoutCompleteRequest", + "description": "Request payload to complete a Checkout. Base checkout schema. Extensions compose onto this using allOf.", + "type": "object" + }, + "CheckoutCreateRequest": { + "required": [ + "line_items" + ], + "properties": { + "line_items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LineItemCreateRequest" + }, + "description": "List of line items being checked out." + }, + "buyer": { + "$ref": "#/components/schemas/Buyer", + "description": "Buyer with consent tracking." + }, + "context": { + "$ref": "#/components/schemas/Context" + }, + "signals": { + "$ref": "#/components/schemas/Signals" + }, + "attribution": { + "$ref": "#/components/schemas/Attribution" + }, + "payment": { + "$ref": "#/components/schemas/Payment" + }, + "discounts": { + "$ref": "#/components/schemas/DiscountsObject" + }, + "fulfillment": { + "$ref": "#/components/schemas/FulfillmentCreateRequest", + "description": "Fulfillment details." + } + }, + "additionalProperties": true, + "title": "CheckoutCreateRequest", + "description": "Request payload to create a new Checkout. Base checkout schema. Extensions compose onto this using allOf.", + "type": "object" + }, + "CheckoutUpdateRequest": { + "required": [ + "line_items" + ], + "properties": { + "line_items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LineItemUpdateRequest" + }, + "description": "List of line items being checked out." + }, + "buyer": { + "$ref": "#/components/schemas/Buyer", + "description": "Buyer with consent tracking." + }, + "context": { + "$ref": "#/components/schemas/Context" + }, + "signals": { + "$ref": "#/components/schemas/Signals" + }, + "attribution": { + "$ref": "#/components/schemas/Attribution" + }, + "payment": { + "$ref": "#/components/schemas/Payment" + }, + "discounts": { + "$ref": "#/components/schemas/DiscountsObject" + }, + "fulfillment": { + "$ref": "#/components/schemas/FulfillmentUpdateRequest", + "description": "Fulfillment details." + } + }, + "additionalProperties": true, + "title": "CheckoutUpdateRequest", + "description": "Request payload to update an existing Checkout. Base checkout schema. Extensions compose onto this using allOf.", + "type": "object" + }, + "Config": { + "type": "object", + "description": "Business browser endpoint configuration for shopping permalinks.", + "required": [ + "endpoint" + ], + "properties": { + "endpoint": { + "$ref": "#/components/schemas/Endpoint" + } + }, + "additionalProperties": true + }, + "Consent": { + "type": "object", + "description": "Per-purpose consent. Keys are reverse-DNS purpose identifiers. UCP defines four well-known purposes: `dev.ucp.consent.marketing`, `dev.ucp.consent.analytics`, `dev.ucp.consent.preferences`, `dev.ucp.consent.sale_or_sharing`. Vendors and merchants may define additional purposes under their own reverse-DNS namespace.", + "propertyNames": { + "$ref": "#/components/schemas/ReverseDomainName" + }, + "additionalProperties": { + "$ref": "#/components/schemas/ConsentPurpose" + } + }, + "ConsentPurpose": { + "type": "object", + "description": "A buyer's consent decision for a purpose (e.g., marketing, analytics). Carries the current binary state, its source (business default or platform-captured buyer decision), human-readable context, and optional refinements scoping the decision to specific channels, vendors, or programs.", + "required": [ + "granted", + "source", + "description" + ], + "properties": { + "granted": { + "type": "boolean", + "description": "Whether consent has been granted for this purpose. The `source` field identifies who asserted this state (business default or platform-captured buyer preference)." + }, + "source": { + "type": "string", + "enum": [ + "business", + "platform" + ], + "description": "Identifies the party that asserted the current `granted` value. `business` means the value reflects the business's default policy; `platform` means the value reflects an explicit buyer decision captured by the platform." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the buyer is consenting to (e.g., 'Promotional communications across all channels')." + }, + "links": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Link" + }, + "description": "Optional links providing context (e.g., privacy policy, terms)." + }, + "segments": { + "type": "object", + "description": "Optional refinements scoping this purpose to specific channels, vendors, or programs. Keys are reverse-DNS identifiers. UCP currently defines two well-known segment identifiers under `dev.ucp.consent.marketing`: `dev.ucp.consent.marketing.email`, `dev.ucp.consent.marketing.sms`. Other segments follow vendor or merchant reverse-DNS conventions.", + "propertyNames": { + "$ref": "#/components/schemas/ReverseDomainName" + }, + "additionalProperties": { + "$ref": "#/components/schemas/ConsentSegment" + } + } + } + }, + "ConsentSegment": { + "type": "object", + "description": "A buyer's consent decision for a specific refinement of a parent purpose (e.g., email marketing under the marketing purpose). Overrides the parent's `granted` value for this scope. Segments do not nest further.", + "required": [ + "granted", + "source", + "description" + ], + "properties": { + "granted": { + "type": "boolean", + "description": "Whether consent has been granted for this segment. Overrides the parent purpose's `granted` value for this specific scope." + }, + "source": { + "type": "string", + "enum": [ + "business", + "platform" + ], + "description": "Identifies the party that asserted the current `granted` value for this segment. `business` means the value reflects the business's default policy; `platform` means the value reflects an explicit buyer decision captured by the platform." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the buyer is consenting to within this segment (e.g., 'Promotional emails and exclusive offers')." + }, + "links": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Link" + }, + "description": "Optional segment-specific links (e.g., channel terms or privacy disclosures)." + } + } + }, + "ConstraintExpression": { + "additionalProperties": false, + "properties": { + "required": { + "type": "array", + "description": "Property names required by the constrained object. Must be non-empty: an empty array applies no constraint.", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "properties": { + "type": "object", + "description": "Constraints keyed by property name. Must be non-empty: an empty object applies no constraint.", + "minProperties": 1, + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/components/schemas/ConstraintExpression" + }, + { + "$ref": "#/components/schemas/ValueConstraint" + } + ] + } + }, + "anyOf": { + "type": "array", + "description": "Alternative Object Constraints. The constrained object must satisfy at least one. A branch must be non-empty: an empty branch is satisfied by every object and neutralizes the alternation.", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/ConstraintExpression", + "minProperties": 1 + } + } + }, + "title": "Constraint Expression", + "description": "A closed JSON Schema Draft 2020-12 constraint expression with Object and Value Constraint positions.", + "type": "object" + }, + "Context": { + "allOf": [ + { + "$ref": "#/components/schemas/Locality" + }, + { + "type": "object", + "additionalProperties": true, + "properties": { + "location": { + "type": "string", + "description": "Stable, opaque identifier for a Location in the Business's namespace. This provisional, non-binding hint is distinct from the Buyer's locality. The operation specification or an active capability/extension defines its effects. A common example in retail shopping is the default home store ID selected and saved by the user when purchasing groceries." + }, + "intent": { + "type": "string", + "description": "Background context describing buyer's intent (e.g., 'looking for a gift under $50', 'need something durable for outdoor use'). Informs relevance, recommendations, and personalization." + }, + "language": { + "type": "string", + "description": "Preferred language for content. Use IETF BCP 47 language tags (e.g., 'en', 'fr-CA', 'zh-Hans'). For REST, equivalent to Accept-Language header—platforms SHOULD fall back to Accept-Language when this field is absent; when provided, overrides Accept-Language. Businesses MAY return content in a different language if unavailable." + }, + "currency": { + "type": "string", + "description": "Preferred currency (ISO 4217, e.g., 'EUR', 'USD'). Businesses determine presentment currency from context and authoritative signals; this hint MAY inform selection in multi-currency markets. Also serves as the denomination for price filter values — platforms SHOULD include this field when sending price filters. Response prices include explicit currency confirming the resolution." + }, + "eligibility": { + "type": "array", + "description": "Buyer claims about eligible benefits such as loyalty membership, payment instrument perks, and similar. Recognized claims MAY inform the Business response (e.g., member-only product availability, adjusted pricing in catalog, provisional discounts at cart or checkout). Businesses MUST ignore unrecognized values without error. Values MUST use reverse-domain naming (e.g., 'com.example.loyalty_gold', 'org.school.student') and MUST be non-identifying.", + "uniqueItems": true, + "items": { + "$ref": "#/components/schemas/ReverseDomainName" + } + }, + "payment": { + "type": "array", + "description": "Buyer-preferred payment handlers in priority order (most preferred first). Each entry names a handler advertised in the Business profile's `ucp.payment_handlers`, optionally narrowed to preferred instrument types. The Business SHOULD use it to preselect or prioritize the handler (and type, when given) and MAY ignore unavailable or ineligible entries; unrecognized values MUST be ignored without error.", + "items": { + "type": "object", + "required": [ + "handler" + ], + "properties": { + "handler": { + "$ref": "#/components/schemas/ReverseDomainName", + "description": "Handler registry key advertised in the Business profile's `ucp.payment_handlers`." + }, + "types": { + "type": "array", + "description": "Optional preferred instrument types for this handler, in priority order, aligned with the handler's advertised `payment_instrument.type` values (for example `card` or `bank`). Unrecognized values MUST be ignored.", + "items": { + "type": "string" + } + } + } + } + } + } + } + ], + "additionalProperties": true, + "title": "Context", + "description": "Provisional buyer signals for relevance and localization—not authoritative data. Businesses SHOULD use these values when verified inputs (e.g., shipping address) are absent, and MAY ignore or down-rank them if inconsistent with higher-confidence signals (authenticated account, risk detection) or regulatory constraints (export controls). Eligibility and policy enforcement MUST occur at checkout time using binding transaction data. Context SHOULD be non-identifying and can be disclosed progressively—coarse signals early, finer resolution as the session progresses. Higher-resolution data (shipping address, billing address) supersedes context.", + "type": "object" + }, + "Description": { + "minProperties": 1, + "properties": { + "plain": { + "type": "string", + "description": "Plain text content." + }, + "html": { + "type": "string", + "description": "HTML-formatted content. Security: Platforms MUST sanitize before rendering—strip scripts, event handlers, and untrusted elements. Treat all rich text as untrusted input." + }, + "markdown": { + "type": "string", + "description": "Markdown-formatted content." + } + }, + "title": "Description", + "description": "Description content in one or more formats. At least one format must be provided.", + "type": "object" + }, + "DetailOptionValue": { + "properties": { + "available": { + "type": "boolean", + "description": "Whether a variant matching this value and the current option selections is purchasable." + }, + "exists": { + "type": "boolean", + "description": "Whether a variant matching this value and the current option selections exists in the catalog." + } + }, + "allOf": [ + { + "$ref": "#/components/schemas/OptionValue" + } + ], + "title": "Detail Option Value", + "description": "An option value with availability signals relative to the current selections. Used in get_product responses where selected context exists.", + "type": "object" + }, + "DetailProduct": { + "description": "A product in a get_product response, extended with effective selections and availability signals on option values.", + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/Product" + } + ], + "properties": { + "selected": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SelectedOption" + }, + "description": "Effective option selections that anchor the featured variant and availability signals. Required when the product has configurable options; may be empty or omitted for products with no option axes." + }, + "options": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "values" + ], + "properties": { + "name": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DetailOptionValue" + }, + "minItems": 1 + } + } + }, + "description": "Product options with availability signals relative to the effective selections." + } + } + }, + "Discount": { + "description": "Extends Cart and Checkout with discount support, including discount codes, automatic discounts, and eligibility-triggered provisional discounts.", + "title": "Discount Extension" + }, + "DiscountsObject": { + "type": "object", + "description": "Discount codes input and applied discounts output.", + "properties": { + "codes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Discount codes to apply. Case-insensitive. Replaces previously submitted codes. Send empty array to clear." + }, + "applied": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AppliedDiscount" + }, + "description": "Discounts successfully applied (code-based and automatic)." + } + } + }, + "EmbeddedConfig": { + "properties": { + "delegate": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Delegations the business allows. At service-level, declares available delegations. In UCP responses, confirms accepted delegations for this session." + }, + "color_scheme": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "light", + "dark" + ] + }, + "description": "Color schemes the business supports. Hosts use ec_color_scheme query parameter to request a scheme from this list." + } + }, + "type": "object", + "title": "Embedded Transport Config", + "description": "Per-session configuration for embedded transport binding. Allows businesses to vary EP availability and delegations based on cart contents, agent authorization, or policy." + }, + "Endpoint": { + "type": "string", + "format": "uri", + "pattern": "^https://[^/?#\\s\\\\@]+(?:/[^?#\\s\\\\]*[^/?#\\s\\\\])?$", + "description": "Absolute HTTPS browser endpoint with a non-empty authority and without userinfo, query, fragment, whitespace, backslashes, or trailing slash. Optional compact item path and query parameters are appended to this endpoint." + }, + "Error": { + "description": "UCP metadata with status 'error'. Use for response branches that carry error information.", + "allOf": [ + { + "$ref": "#/components/schemas/UcpBase" + }, + { + "properties": { + "status": { + "const": "error", + "default": "error", + "type": "string" + } + }, + "required": [ + "status" + ] + } + ] + }, + "ErrorCode": { + "examples": [ + "not_found", + "out_of_stock", + "item_unavailable", + "address_undeliverable", + "payment_failed", + "eligibility_invalid", + "identity_required", + "insufficient_scope" + ], + "type": "string", + "title": "Error Code", + "description": "Error code identifying the type of error. Standard errors are defined in capability specifications (see examples) and have standardized semantics; freeform codes are permitted." + }, + "ErrorResponse": { + "properties": { + "ucp": { + "$ref": "#/components/schemas/Error", + "description": "UCP protocol metadata. Status MUST be 'error' for error response." + }, + "messages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + "minItems": 1, + "description": "Array of messages describing why the operation failed." + }, + "continue_url": { + "type": "string", + "format": "uri", + "description": "URL for buyer handoff or session recovery." + } + }, + "additionalProperties": false, + "title": "Error Response", + "description": "Generic error response when business logic prevents resource creation or failed to retrieve resource. Used when no valid resource can be established.", + "type": "object", + "required": [ + "ucp", + "messages" + ] + }, + "Expectation": { + "properties": { + "id": { + "type": "string", + "description": "Expectation identifier." + }, + "line_items": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "quantity" + ], + "properties": { + "id": { + "type": "string", + "description": "Line item ID reference." + }, + "quantity": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991, + "description": "Integer count of steps of the referenced line item's `quantity_unit` (`10^-scale` × `unit`); when `quantity_unit` is absent, it counts whole items (`each`)." + } + } + }, + "description": "Which line items and quantities are in this expectation." + }, + "method_type": { + "type": "string", + "description": "Delivery method type. Well-known values: `shipping`, `pickup`, `digital`; additional values MAY be used." + }, + "destination": { + "$ref": "#/components/schemas/PostalAddress", + "description": "Delivery destination address." + }, + "description": { + "type": "string", + "description": "Human-readable delivery description (e.g., 'Arrives in 5-8 business days')." + }, + "fulfillable_on": { + "type": "string", + "description": "When this expectation can be fulfilled: 'now' or ISO 8601 timestamp for future date (backorder, pre-order)." + } + }, + "required": [ + "id", + "line_items", + "method_type", + "destination" + ], + "title": "Expectation", + "description": "Buyer-facing fulfillment expectation representing logical groupings of items (e.g., 'package'). Can be split, merged, or adjusted post-order to set buyer expectations for when/how items arrive.", + "type": "object" + }, + "Fulfillment": { + "properties": { + "methods": { + "type": "array", + "description": "Fulfillment methods for cart items.", + "items": { + "$ref": "#/components/schemas/FulfillmentMethod" + } + }, + "available_methods": { + "type": "array", + "description": "Inventory availability hints.", + "items": { + "$ref": "#/components/schemas/FulfillmentAvailableMethod" + } + } + }, + "type": "object", + "title": "Fulfillment", + "description": "Container for fulfillment methods and availability." + }, + "FulfillmentAvailableMethod": { + "required": [ + "type", + "line_item_ids" + ], + "properties": { + "type": { + "type": "string", + "description": "Fulfillment method type this availability applies to. Well-known values: `shipping`, `pickup`; businesses MAY use additional values." + }, + "line_item_ids": { + "type": "array", + "description": "Line items available for this fulfillment method.", + "items": { + "type": "string" + } + }, + "fulfillable_on": { + "type": [ + "string", + "null" + ], + "description": "'now' for immediate availability, or ISO 8601 date for future (preorders, transfers)." + }, + "description": { + "type": "string", + "description": "Human-readable availability info (e.g., 'Available for pickup at Downtown Store today')." + } + }, + "title": "Fulfillment Available Method", + "description": "Inventory availability hint for a fulfillment method type.", + "type": "object", + "additionalProperties": true + }, + "FulfillmentCreateRequest": { + "properties": { + "methods": { + "type": "array", + "description": "Fulfillment methods for cart items.", + "items": { + "$ref": "#/components/schemas/FulfillmentMethodCreateRequest" + } + } + }, + "type": "object", + "title": "FulfillmentCreateRequest", + "description": "Request payload to create a new Fulfillment. Container for fulfillment methods and availability." + }, + "FulfillmentDestination": { + "description": "A destination for fulfillment.", + "type": "object", + "title": "Fulfillment Destination", + "oneOf": [ + { + "$ref": "#/components/schemas/LocationDestination" + }, + { + "$ref": "#/components/schemas/ShippingDestination" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "business_location": "#/components/schemas/LocationDestination", + "shipping_address": "#/components/schemas/ShippingDestination" + } + } + }, + "FulfillmentDestinationCreateRequest": { + "description": "Request payload to create a new FulfillmentDestination. A destination for fulfillment.", + "type": "object", + "title": "FulfillmentDestinationCreateRequest", + "oneOf": [ + { + "$ref": "#/components/schemas/LocationDestinationCreateRequest" + }, + { + "$ref": "#/components/schemas/ShippingDestinationCreateRequest" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "business_location": "#/components/schemas/LocationDestinationCreateRequest", + "shipping_address": "#/components/schemas/ShippingDestinationCreateRequest" + } + } + }, + "FulfillmentDestinationFilter": { + "allOf": [ + { + "$ref": "#/components/schemas/Locality" + }, + { + "type": "object", + "additionalProperties": true, + "properties": { + "location": { + "type": "string", + "description": "A reference to the destination (e.g. store, pickup location, saved address)." + } + } + } + ], + "additionalProperties": true, + "title": "Fulfillment Destination Filter", + "description": "A specific destination, named by value or by reference: a coarse locality (`address_country` / `address_region` / `postal_code`), or a `location` id. Platforms SHOULD provide one or the other, not both; if both are present, a business SHOULD use the more specific — typically `location`.", + "type": "object" + }, + "FulfillmentDestinationUpdateRequest": { + "description": "Request payload to update an existing FulfillmentDestination. A destination for fulfillment.", + "type": "object", + "title": "FulfillmentDestinationUpdateRequest", + "oneOf": [ + { + "$ref": "#/components/schemas/LocationDestinationUpdateRequest" + }, + { + "$ref": "#/components/schemas/ShippingDestinationUpdateRequest" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "business_location": "#/components/schemas/LocationDestinationUpdateRequest", + "shipping_address": "#/components/schemas/ShippingDestinationUpdateRequest" + } + } + }, + "FulfillmentDetailProduct": { + "description": "A get_product detail product (carrying selected/options availability signals) whose variants are fulfillment-enriched. Used by get_product.", + "allOf": [ + { + "$ref": "#/components/schemas/DetailProduct" + }, + { + "type": "object", + "properties": { + "variants": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FulfillmentVariant" + } + } + } + } + ] + }, + "FulfillmentEvent": { + "properties": { + "id": { + "type": "string", + "description": "Fulfillment event identifier." + }, + "occurred_at": { + "type": "string", + "format": "date-time", + "description": "RFC 3339 timestamp when this fulfillment event occurred." + }, + "type": { + "type": "string", + "description": "Fulfillment event type. Common values include: processing (preparing to ship), shipped (handed to carrier), in_transit (in delivery network), delivered (received by buyer), failed_attempt (delivery attempt failed), canceled (fulfillment canceled), undeliverable (cannot be delivered), returned_to_sender (returned to merchant)." + }, + "line_items": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "quantity" + ], + "properties": { + "id": { + "type": "string", + "description": "Line item ID reference." + }, + "quantity": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991, + "description": "Integer count of steps of the referenced line item's `quantity_unit` (`10^-scale` × `unit`); when `quantity_unit` is absent, it counts whole items (`each`)." + } + } + }, + "description": "Which line items and quantities are fulfilled in this event." + }, + "tracking_number": { + "type": "string", + "description": "Carrier tracking number (required if type != processing)." + }, + "tracking_url": { + "type": "string", + "format": "uri", + "description": "URL to track this shipment (required if type != processing)." + }, + "carrier": { + "type": "string", + "description": "Carrier name (e.g., 'FedEx', 'USPS')." + }, + "description": { + "type": "string", + "description": "Human-readable description of the shipment status or delivery information (e.g., 'Delivered to front door', 'Out for delivery')." + } + }, + "required": [ + "id", + "occurred_at", + "type", + "line_items" + ], + "title": "Fulfillment Event", + "description": "Append-only fulfillment event representing an actual shipment. References line items by ID.", + "type": "object" + }, + "FulfillmentGetProductRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/GetProductRequest" + }, + { + "type": "object", + "properties": { + "filters": { + "$ref": "#/components/schemas/FulfillmentSearchFilters" + } + } + } + ] + }, + "FulfillmentGetProductResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/GetProductResponse" + }, + { + "type": "object", + "properties": { + "product": { + "$ref": "#/components/schemas/FulfillmentDetailProduct" + } + } + } + ] + }, + "FulfillmentGroup": { + "required": [ + "id", + "line_item_ids" + ], + "properties": { + "id": { + "type": "string", + "description": "Group identifier for referencing merchant-generated groups in updates." + }, + "line_item_ids": { + "type": "array", + "description": "Line item IDs included in this group/package.", + "items": { + "type": "string" + } + }, + "options": { + "type": "array", + "description": "Available fulfillment options for this group.", + "items": { + "$ref": "#/components/schemas/FulfillmentOption" + } + }, + "selected_option_id": { + "type": [ + "string", + "null" + ], + "description": "ID of the selected fulfillment option for this group." + } + }, + "title": "Fulfillment Group", + "description": "A merchant-generated package/group of line items with fulfillment options.", + "type": "object", + "additionalProperties": true + }, + "FulfillmentGroupCreateRequest": { + "additionalProperties": true, + "properties": { + "selected_option_id": { + "type": [ + "string", + "null" + ], + "description": "ID of the selected fulfillment option for this group." + } + }, + "title": "FulfillmentGroupCreateRequest", + "description": "Request payload to create a new FulfillmentGroup. A merchant-generated package/group of line items with fulfillment options.", + "type": "object" + }, + "FulfillmentGroupUpdateRequest": { + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "description": "Group identifier for referencing merchant-generated groups in updates." + }, + "selected_option_id": { + "type": [ + "string", + "null" + ], + "description": "ID of the selected fulfillment option for this group." + } + }, + "title": "FulfillmentGroupUpdateRequest", + "description": "Request payload to update an existing FulfillmentGroup. A merchant-generated package/group of line items with fulfillment options.", + "type": "object", + "additionalProperties": true + }, + "FulfillmentLookupProduct": { + "description": "A lookup product whose variants are fulfillment-enriched, preserving input correlation. Used by lookup.", + "allOf": [ + { + "$ref": "#/components/schemas/Product" + }, + { + "type": "object", + "properties": { + "variants": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FulfillmentLookupVariant" + } + } + } + } + ] + }, + "FulfillmentLookupRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/LookupRequest" + }, + { + "type": "object", + "properties": { + "filters": { + "$ref": "#/components/schemas/FulfillmentSearchFilters" + } + } + } + ] + }, + "FulfillmentLookupResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/LookupResponse" + }, + { + "type": "object", + "properties": { + "products": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FulfillmentLookupProduct" + } + } + } + } + ] + }, + "FulfillmentLookupVariant": { + "description": "A lookup variant (carrying input correlation) enriched with fulfillment.", + "allOf": [ + { + "$ref": "#/components/schemas/LookupVariant" + }, + { + "type": "object", + "properties": { + "fulfillment": { + "$ref": "#/components/schemas/CatalogFulfillment" + } + } + } + ] + }, + "FulfillmentMethod": { + "required": [ + "id", + "type", + "line_item_ids" + ], + "dependentRequired": { + "destinations": [ + "type" + ] + }, + "title": "Fulfillment Method", + "description": "A fulfillment method with destinations and groups.", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique fulfillment method identifier." + }, + "type": { + "type": "string", + "description": "Fulfillment method type. Well-known values: `shipping`, `pickup`. Businesses MAY use additional values." + }, + "line_item_ids": { + "type": "array", + "description": "Line item IDs fulfilled via this method.", + "items": { + "type": "string" + } + }, + "destinations": { + "type": "array", + "description": "Available destinations for this method. In Business responses, each destination carries a `type` and `id`.", + "items": { + "$ref": "#/components/schemas/FulfillmentDestination" + } + }, + "selected_destination_id": { + "type": [ + "string", + "null" + ], + "description": "ID of the selected destination. Accepts any stable, Business-scoped ID the Business recognizes for this method, including Location IDs not yet enumerated in `destinations`." + }, + "groups": { + "type": "array", + "description": "Fulfillment groups for selecting options. Agent sets selected_option_id on groups to choose shipping method.", + "items": { + "$ref": "#/components/schemas/FulfillmentGroup" + } + } + }, + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "shipping", + "default": "shipping", + "type": "string" + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "destinations": { + "type": "array", + "description": "Platform-authored shipping addresses for this method.", + "items": { + "$ref": "#/components/schemas/ShippingDestination" + } + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "pickup", + "default": "pickup", + "type": "string" + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "destinations": { + "type": "array", + "description": "Business-authored business locations able to fulfill this method. Response-only: the Platform selects one via `selected_destination_id`, never writes one.", + "items": { + "$ref": "#/components/schemas/LocationDestination" + } + } + } + } + } + ] + }, + "FulfillmentMethodCreateRequest": { + "required": [ + "type" + ], + "dependentRequired": { + "destinations": [ + "type" + ] + }, + "title": "FulfillmentMethodCreateRequest", + "description": "Request payload to create a new FulfillmentMethod. A fulfillment method with destinations and groups.", + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Fulfillment method type. Well-known values: `shipping`, `pickup`. Businesses MAY use additional values." + }, + "selected_destination_id": { + "type": [ + "string", + "null" + ], + "description": "ID of the selected destination. Accepts any stable, Business-scoped ID the Business recognizes for this method, including Location IDs not yet enumerated in `destinations`." + }, + "groups": { + "type": "array", + "description": "Fulfillment groups for selecting options. Agent sets selected_option_id on groups to choose shipping method.", + "items": { + "$ref": "#/components/schemas/FulfillmentGroupCreateRequest" + } + } + }, + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "shipping", + "default": "shipping", + "type": "string" + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "destinations": { + "type": "array", + "description": "Platform-authored shipping addresses for this method.", + "items": { + "$ref": "#/components/schemas/ShippingDestinationCreateRequest" + } + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "pickup", + "default": "pickup", + "type": "string" + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": {} + } + } + ] + }, + "FulfillmentMethodUpdateRequest": { + "required": [ + "line_item_ids" + ], + "dependentRequired": { + "destinations": [ + "type" + ] + }, + "title": "FulfillmentMethodUpdateRequest", + "description": "Request payload to update an existing FulfillmentMethod. A fulfillment method with destinations and groups.", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique fulfillment method identifier." + }, + "type": { + "type": "string", + "description": "Fulfillment method type. Well-known values: `shipping`, `pickup`. Businesses MAY use additional values." + }, + "line_item_ids": { + "type": "array", + "description": "Line item IDs fulfilled via this method.", + "items": { + "type": "string" + } + }, + "selected_destination_id": { + "type": [ + "string", + "null" + ], + "description": "ID of the selected destination. Accepts any stable, Business-scoped ID the Business recognizes for this method, including Location IDs not yet enumerated in `destinations`." + }, + "groups": { + "type": "array", + "description": "Fulfillment groups for selecting options. Agent sets selected_option_id on groups to choose shipping method.", + "items": { + "$ref": "#/components/schemas/FulfillmentGroupUpdateRequest" + } + } + }, + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "shipping", + "default": "shipping", + "type": "string" + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "destinations": { + "type": "array", + "description": "Platform-authored shipping addresses for this method.", + "items": { + "$ref": "#/components/schemas/ShippingDestinationUpdateRequest" + } + } + } + } + }, + { + "if": { + "properties": { + "type": { + "const": "pickup", + "default": "pickup", + "type": "string" + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": {} + } + } + ] + }, + "FulfillmentOption": { + "allOf": [ + { + "$ref": "#/components/schemas/FulfillmentOptionBase" + }, + { + "type": "object", + "additionalProperties": true, + "properties": { + "carrier": { + "type": "string", + "description": "Carrier name (for shipping)." + }, + "earliest_fulfillment_time": { + "type": "string", + "format": "date-time", + "description": "Earliest fulfillment date." + }, + "latest_fulfillment_time": { + "type": "string", + "format": "date-time", + "description": "Latest fulfillment date." + }, + "totals": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Total" + }, + "description": "Fulfillment option totals breakdown." + } + }, + "required": [ + "totals" + ] + } + ], + "type": "object", + "title": "Fulfillment Option", + "description": "A fulfillment option within a group (e.g., Standard Shipping $5, Express $15). Extends the fulfillment option base with cost and timing." + }, + "FulfillmentOptionBase": { + "required": [ + "id", + "title" + ], + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for this fulfillment option." + }, + "title": { + "type": "string", + "description": "Short label that distinguishes this option from its siblings (e.g. 'Standard', 'Express Shipping', 'Curbside Pickup')." + }, + "description": { + "$ref": "#/components/schemas/Description", + "description": "Supplementary context for the title (e.g. 'Arrives in 4 business days', 'Arrives Dec 12-15 via FedEx'). Directly renderable; MUST NOT repeat the title." + } + }, + "title": "Fulfillment Option Base", + "description": "Common base for a fulfillment option: an addressable, renderable choice (e.g. Standard, Express). Catalog uses this base directly; checkout composes it with cost and timing.", + "type": "object", + "additionalProperties": true + }, + "FulfillmentOptionCreateRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/FulfillmentOptionBase" + }, + { + "type": "object", + "additionalProperties": true, + "properties": {} + } + ], + "type": "object", + "title": "FulfillmentOptionCreateRequest", + "description": "Request payload to create a new FulfillmentOption. A fulfillment option within a group (e.g., Standard Shipping $5, Express $15). Extends the fulfillment option base with cost and timing." + }, + "FulfillmentOptionUpdateRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/FulfillmentOptionBase" + }, + { + "type": "object", + "additionalProperties": true, + "properties": {} + } + ], + "type": "object", + "title": "FulfillmentOptionUpdateRequest", + "description": "Request payload to update an existing FulfillmentOption. A fulfillment option within a group (e.g., Standard Shipping $5, Express $15). Extends the fulfillment option base with cost and timing." + }, + "FulfillmentProduct": { + "description": "A catalog product whose variants are fulfillment-enriched. Used by search.", + "allOf": [ + { + "$ref": "#/components/schemas/Product" + }, + { + "type": "object", + "properties": { + "variants": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FulfillmentVariant" + } + } + } + } + ] + }, + "FulfillmentSearchFilters": { + "description": "Catalog filters extended with a fulfillment destination filter and a method-type filter.", + "allOf": [ + { + "$ref": "#/components/schemas/SearchFilters" + }, + { + "type": "object", + "properties": { + "fulfills_to": { + "$ref": "#/components/schemas/FulfillmentDestinationFilter", + "description": "Explicit destination where items are fulfilled. It may differ from the locality or Business Location supplied in `context` (e.g. a gift delivered directly to the recipient). The filter restricts results to what can be fulfilled there and seeds method `availability`. It supersedes `context` only for fulfillment destination and availability resolution." + }, + "methods": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Restrict results to these fulfillment method types (e.g. [\"pickup\"]). Well-known values: `shipping`, `pickup`." + } + } + } + ] + }, + "FulfillmentSearchRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/SearchRequest" + }, + { + "type": "object", + "properties": { + "filters": { + "$ref": "#/components/schemas/FulfillmentSearchFilters" + } + } + } + ] + }, + "FulfillmentSearchResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/SearchResponse" + }, + { + "type": "object", + "properties": { + "products": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FulfillmentProduct" + } + } + } + } + ] + }, + "FulfillmentUpdateRequest": { + "properties": { + "methods": { + "type": "array", + "description": "Fulfillment methods for cart items.", + "items": { + "$ref": "#/components/schemas/FulfillmentMethodUpdateRequest" + } + } + }, + "type": "object", + "title": "FulfillmentUpdateRequest", + "description": "Request payload to update an existing Fulfillment. Container for fulfillment methods and availability." + }, + "FulfillmentVariant": { + "description": "A catalog variant with fulfillment.", + "allOf": [ + { + "$ref": "#/components/schemas/Variant" + }, + { + "type": "object", + "properties": { + "fulfillment": { + "$ref": "#/components/schemas/CatalogFulfillment" + } + } + } + ] + }, + "GetProductRequest": { + "type": "object", + "description": "Request body for single-product retrieval. Supports interactive variant narrowing via selected and preferences.", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "description": "Product or variant identifier. Implementations MUST support product ID and variant ID." + }, + "selected": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SelectedOption" + }, + "description": "Partial or full option selections for interactive variant narrowing. When provided, response option values include availability signals (available, exists) relative to these selections." + }, + "preferences": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Option names in relaxation priority order. When no exact variant matches all selections, the server drops options from the end of this list first. E.g., ['Color', 'Size'] keeps Color and relaxes Size." + }, + "filters": { + "$ref": "#/components/schemas/SearchFilters", + "description": "Filter criteria to narrow returned variants. All specified filters combine with AND logic." + }, + "context": { + "$ref": "#/components/schemas/Context" + }, + "signals": { + "$ref": "#/components/schemas/Signals" + }, + "attribution": { + "$ref": "#/components/schemas/Attribution" + } + } + }, + "GetProductResponse": { + "type": "object", + "required": [ + "ucp", + "product" + ], + "properties": { + "ucp": { + "$ref": "#/components/schemas/ResponseCatalogSchema" + }, + "product": { + "$ref": "#/components/schemas/DetailProduct", + "description": "The requested product with full detail. Singular — this is a single-resource operation." + }, + "actions": { + "$ref": "#/components/schemas/Actions", + "description": "Outstanding extension-defined Actions for this product response." + }, + "messages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + "description": "Warnings or informational messages about the product (e.g., price recently changed, limited availability)." + }, + "policies": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Policy" + }, + "description": "Policies (e.g., return/refund terms) that apply to this product. `applies_to` targets are relative to the response root; when absent or empty, refer to the URLs in `links[]`." + } + } + }, + "InfoCode": { + "examples": [ + "identity_optional", + "signal", + "free_shipping", + "not_found" + ], + "type": "string", + "title": "Info Code", + "description": "Info code identifying the type of informational message. Standard codes are defined in capability specifications (see examples) and have standardized semantics; freeform codes are permitted." + }, + "InputCorrelation": { + "properties": { + "id": { + "type": "string", + "description": "The identifier from the lookup request that resolved to this variant." + }, + "match": { + "type": "string", + "description": "How the request identifier resolved to this variant. Well-known values: `exact` (input directly identifies this variant, e.g., variant ID, SKU), `featured` (server selected this variant as representative, e.g., product ID resolved to best match). Businesses MAY implement and provide additional resolution strategies.", + "examples": [ + "exact", + "featured" + ] + } + }, + "required": [ + "id" + ], + "title": "Input Correlation", + "description": "Maps a request identifier to the variant it resolved to, with match semantics.", + "type": "object" + }, + "Instance": { + "type": "object", + "description": "Common fields for one outstanding Action instance are id and optional config. The extension declaring the Action type defines type-specific processing data under config. Additional properties are permitted for forward compatibility.", + "required": [ + "id" + ], + "additionalProperties": true, + "properties": { + "id": { + "type": "string", + "minLength": 1, + "description": "Identifier for this Action instance." + }, + "config": { + "type": "object", + "description": "Configuration defined by the extension that declares this Action type.", + "additionalProperties": true + } + } + }, + "Item": { + "required": [ + "id", + "title", + "price" + ], + "properties": { + "id": { + "type": "string", + "description": "The product identifier, often the SKU, required to resolve the product details associated with this line item. Should be recognized by both the Platform, and the Business." + }, + "title": { + "type": "string", + "description": "Product title." + }, + "price": { + "$ref": "#/components/schemas/Amount", + "description": "Unit price in ISO 4217 minor units. Price is the amount per one whole `quantity_unit.unit` (for example, per lb or per hour); when `quantity_unit` is absent, it is per `each`." + }, + "quantity_unit": { + "$ref": "#/components/schemas/QuantityUnit", + "description": "Sale basis this item's `quantity` is denominated in. On an authoritative Business response, absence encodes the default `each` machine identity (`C62`, 0); the Business MUST include this descriptor for every non-`each` response. On Platform requests, omission makes no assertion: the Business interprets `quantity` using the item's authoritative sale basis. If the Platform includes this descriptor, it asserts the unit-descriptor machine identity. The Business MUST compare that machine identity (`unit`, effective `scale`), ignore `display_text` and `increment`, and resolve a mismatch by conversion surfaced as a visible line revision with a warning, or by rejection with a recoverable business outcome; silent reinterpretation is forbidden. An explicit `C62` descriptor at effective scale 0 matches an authoritative basis represented by an absent descriptor." + }, + "unit_price": { + "$ref": "#/components/schemas/UnitPrice", + "description": "Pricing basis for this item. On an authoritative Business response, the Business MUST include `unit_price` on every line whose pricing basis differs from its sale basis (for example, priced per pound but sold per `each`); presence on a line marks the rate as transactional rather than display-only. When the pricing basis is the sale basis, `item.price` fully denominates the charge and this field MAY be omitted." + }, + "image_url": { + "type": "string", + "description": "Product image URI.", + "format": "uri" + } + }, + "title": "Item", + "type": "object" + }, + "ItemCreateRequest": { + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "description": "The product identifier, often the SKU, required to resolve the product details associated with this line item. Should be recognized by both the Platform, and the Business." + }, + "quantity_unit": { + "$ref": "#/components/schemas/QuantityUnit", + "description": "Sale basis this item's `quantity` is denominated in. On an authoritative Business response, absence encodes the default `each` machine identity (`C62`, 0); the Business MUST include this descriptor for every non-`each` response. On Platform requests, omission makes no assertion: the Business interprets `quantity` using the item's authoritative sale basis. If the Platform includes this descriptor, it asserts the unit-descriptor machine identity. The Business MUST compare that machine identity (`unit`, effective `scale`), ignore `display_text` and `increment`, and resolve a mismatch by conversion surfaced as a visible line revision with a warning, or by rejection with a recoverable business outcome; silent reinterpretation is forbidden. An explicit `C62` descriptor at effective scale 0 matches an authoritative basis represented by an absent descriptor." + } + }, + "title": "ItemCreateRequest", + "type": "object", + "description": "Request payload to create a new Item." + }, + "ItemUpdateRequest": { + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "description": "The product identifier, often the SKU, required to resolve the product details associated with this line item. Should be recognized by both the Platform, and the Business." + }, + "quantity_unit": { + "$ref": "#/components/schemas/QuantityUnit", + "description": "Sale basis this item's `quantity` is denominated in. On an authoritative Business response, absence encodes the default `each` machine identity (`C62`, 0); the Business MUST include this descriptor for every non-`each` response. On Platform requests, omission makes no assertion: the Business interprets `quantity` using the item's authoritative sale basis. If the Platform includes this descriptor, it asserts the unit-descriptor machine identity. The Business MUST compare that machine identity (`unit`, effective `scale`), ignore `display_text` and `increment`, and resolve a mismatch by conversion surfaced as a visible line revision with a warning, or by rejection with a recoverable business outcome; silent reinterpretation is forbidden. An explicit `C62` descriptor at effective scale 0 matches an authoritative basis represented by an absent descriptor." + } + }, + "title": "ItemUpdateRequest", + "type": "object", + "description": "Request payload to update an existing Item." + }, + "JwkPublicKey": { + "type": "object", + "description": "Public JSON Web Key used for HTTP Message Signatures and signed webhook verification. UCP profiles publish public keys only; private key material MUST NOT appear in a profile. Well-known key types: EC (ECDSA P-256, P-384) and OKP (EdDSA Ed25519); OKP keys are RECOMMENDED for signers opting into Web Bot Auth (WBA) interop on HTTP transport. A single profile MAY publish keys of either or both types; consumers select keys by kid. The kty, crv, and alg vocabularies are OPEN: verifiers MUST tolerate key types, curves, and algorithms they do not recognize, selecting keys by kid at verification time. An unsupported key affects only the signature that references it (algorithm_unsupported) and MUST NOT cause whole-profile rejection. Additional public JWK members are permitted; consumers ignore unknown members.", + "required": [ + "kid", + "kty" + ], + "properties": { + "kid": { + "type": "string", + "description": "Key identifier referenced by Signature-Input keyid. For keys used in dual-audience (Web Bot Auth) signatures, the kid MUST be the key's JWK SHA-256 Thumbprint (RFC 7638) so UCP-Agent and Signature-Agent lookups resolve the same key; otherwise the kid MAY be any stable string." + }, + "kty": { + "type": "string", + "examples": [ + "EC", + "OKP" + ], + "description": "JWK key type. Well-known values: EC for ECDSA (P-256, P-384); OKP for EdDSA (Ed25519). Open vocabulary; verifiers tolerate unrecognized types and select keys by kid." + }, + "crv": { + "type": "string", + "examples": [ + "P-256", + "P-384", + "Ed25519" + ], + "description": "Curve name. Well-known values: P-256, P-384 (EC); Ed25519 (OKP). Open vocabulary." + }, + "x": { + "type": "string", + "description": "Public key value, base64url-encoded. For EC, the x coordinate (RFC 7518 §6.2); for OKP, the public key (RFC 8037 §2)." + }, + "y": { + "type": "string", + "description": "EC public key y coordinate, base64url-encoded (RFC 7518 §6.2). Not used by OKP keys." + }, + "alg": { + "type": "string", + "examples": [ + "ES256", + "ES384", + "EdDSA" + ], + "description": "JWA algorithm associated with this public key. Optional; verifiers derive the algorithm from crv when alg is omitted. When present for a well-known curve it MUST match: ES256 with P-256, ES384 with P-384, EdDSA with Ed25519." + }, + "use": { + "type": "string", + "description": "JWK public key use. UCP examples use sig for signatures." + } + }, + "allOf": [ + { + "title": "EC keys carry crv, x, y", + "if": { + "properties": { + "kty": { + "const": "EC", + "default": "EC", + "type": "string" + } + }, + "required": [ + "kty" + ] + }, + "then": { + "required": [ + "crv", + "x", + "y" + ] + } + }, + { + "title": "OKP keys carry crv, x", + "if": { + "properties": { + "kty": { + "const": "OKP", + "default": "OKP", + "type": "string" + } + }, + "required": [ + "kty" + ] + }, + "then": { + "required": [ + "crv", + "x" + ] + } + }, + { + "title": "P-256 pairs with ES256", + "if": { + "properties": { + "crv": { + "const": "P-256", + "default": "P-256", + "type": "string" + } + }, + "required": [ + "crv" + ] + }, + "then": { + "properties": { + "alg": { + "const": "ES256", + "default": "ES256", + "type": "string" + } + } + } + }, + { + "title": "P-384 pairs with ES384", + "if": { + "properties": { + "crv": { + "const": "P-384", + "default": "P-384", + "type": "string" + } + }, + "required": [ + "crv" + ] + }, + "then": { + "properties": { + "alg": { + "const": "ES384", + "default": "ES384", + "type": "string" + } + } + } + }, + { + "title": "Ed25519 pairs with EdDSA", + "if": { + "properties": { + "crv": { + "const": "Ed25519", + "default": "Ed25519", + "type": "string" + } + }, + "required": [ + "crv" + ] + }, + "then": { + "properties": { + "alg": { + "const": "EdDSA", + "default": "EdDSA", + "type": "string" + } + } + } + } + ], + "not": { + "anyOf": [ + { + "required": [ + "d" + ] + }, + { + "required": [ + "p" + ] + }, + { + "required": [ + "q" + ] + }, + { + "required": [ + "dp" + ] + }, + { + "required": [ + "dq" + ] + }, + { + "required": [ + "qi" + ] + }, + { + "required": [ + "oth" + ] + }, + { + "required": [ + "k" + ] + } + ] + }, + "additionalProperties": true + }, + "LineItem": { + "required": [ + "id", + "item", + "quantity", + "totals" + ], + "properties": { + "id": { + "type": "string" + }, + "item": { + "$ref": "#/components/schemas/Item" + }, + "quantity": { + "type": "integer", + "description": "Always an integer step count. On Platform requests, steps use the item's Business-authoritative sale basis; omitting `item.quantity_unit` makes no assertion and does not imply `each`. On Business responses, `item.quantity_unit` describes the basis; if absent, it encodes the `each` machine identity (`C62`, 0) and `quantity` counts whole items.", + "minimum": 1, + "maximum": 9007199254740991 + }, + "totals": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Total" + }, + "description": "Line item totals breakdown." + }, + "parent_id": { + "type": "string", + "description": "Parent line item identifier for any nested structures." + } + }, + "title": "Line Item", + "type": "object", + "description": "Line item object. Expected to use the currency of the parent object." + }, + "LineItemCreateRequest": { + "required": [ + "item", + "quantity" + ], + "properties": { + "item": { + "$ref": "#/components/schemas/ItemCreateRequest" + }, + "quantity": { + "type": "integer", + "description": "Always an integer step count. On Platform requests, steps use the item's Business-authoritative sale basis; omitting `item.quantity_unit` makes no assertion and does not imply `each`. On Business responses, `item.quantity_unit` describes the basis; if absent, it encodes the `each` machine identity (`C62`, 0) and `quantity` counts whole items.", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "title": "LineItemCreateRequest", + "type": "object", + "description": "Request payload to create a new LineItem. Line item object. Expected to use the currency of the parent object." + }, + "LineItemUpdateRequest": { + "required": [ + "item", + "quantity" + ], + "properties": { + "id": { + "type": "string" + }, + "item": { + "$ref": "#/components/schemas/ItemUpdateRequest" + }, + "quantity": { + "type": "integer", + "description": "Always an integer step count. On Platform requests, steps use the item's Business-authoritative sale basis; omitting `item.quantity_unit` makes no assertion and does not imply `each`. On Business responses, `item.quantity_unit` describes the basis; if absent, it encodes the `each` machine identity (`C62`, 0) and `quantity` counts whole items.", + "minimum": 1, + "maximum": 9007199254740991 + }, + "parent_id": { + "type": "string", + "description": "Parent line item identifier for any nested structures." + } + }, + "title": "LineItemUpdateRequest", + "type": "object", + "description": "Request payload to update an existing LineItem. Line item object. Expected to use the currency of the parent object." + }, + "Link": { + "properties": { + "type": { + "type": "string", + "description": "Type of link. Well-known values: `privacy_policy`, `terms_of_service`, `refund_policy`, `shipping_policy`, `faq`. Consumers SHOULD handle unknown values gracefully by displaying them using the `title` field or omitting the link." + }, + "url": { + "type": "string", + "description": "The actual URL pointing to the content to be displayed.", + "format": "uri" + }, + "title": { + "type": "string", + "description": "Optional display text for the link. When provided, use this instead of generating from type." + } + }, + "required": [ + "type", + "url" + ], + "title": "Link", + "type": "object" + }, + "Locality": { + "properties": { + "address_country": { + "type": "string", + "description": "The country, as a 2-letter ISO 3166-1 alpha-2 code (e.g. \"US\"). A 3-letter alpha-3 code or full country name MAY also be used." + }, + "address_region": { + "type": "string", + "description": "The first-level administrative region within the country (e.g. a state or province such as California)." + }, + "postal_code": { + "type": "string", + "description": "The postal code (e.g. \"94043\")." + } + }, + "additionalProperties": true, + "title": "Locality", + "description": "A coarse geographic location — country, region, and postal code. A lightweight alternative to a full postal address.", + "type": "object" + }, + "LocationDestination": { + "allOf": [ + { + "$ref": "#/components/schemas/LocationSummary" + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "business_location", + "description": "Destination type discriminator. Response-only.", + "default": "business_location" + } + }, + "required": [ + "type" + ] + } + ], + "type": "object", + "title": "Business Location Destination", + "description": "A business location fulfillment destination. Business-authored and response-only: the Platform selects a location via `selected_destination_id` rather than writing destinations." + }, + "LocationDestinationCreateRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/LocationSummaryCreateRequest" + }, + { + "type": "object", + "properties": {} + } + ], + "type": "object", + "title": "LocationDestinationCreateRequest", + "description": "Request payload to create a new LocationDestination. A business location fulfillment destination. Business-authored and response-only: the Platform selects a location via `selected_destination_id` rather than writing destinations." + }, + "LocationDestinationUpdateRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/LocationSummaryUpdateRequest" + }, + { + "type": "object", + "properties": {} + } + ], + "type": "object", + "title": "LocationDestinationUpdateRequest", + "description": "Request payload to update an existing LocationDestination. A business location fulfillment destination. Business-authored and response-only: the Platform selects a location via `selected_destination_id` rather than writing destinations." + }, + "LocationSummary": { + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "string", + "description": "Stable, opaque, Business-scoped Location identifier." + }, + "name": { + "type": "string", + "description": "Buyer-facing, Business-owned display name." + }, + "address": { + "$ref": "#/components/schemas/PostalAddress", + "description": "Physical address of the location." + } + }, + "title": "Location Summary", + "description": "A summary of a physical business location.", + "type": "object", + "additionalProperties": true + }, + "LocationSummaryCreateRequest": { + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "description": "Stable, opaque, Business-scoped Location identifier." + } + }, + "title": "LocationSummaryCreateRequest", + "description": "Request payload to create a new LocationSummary. A summary of a physical business location.", + "type": "object", + "additionalProperties": true + }, + "LocationSummaryUpdateRequest": { + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "description": "Stable, opaque, Business-scoped Location identifier." + } + }, + "title": "LocationSummaryUpdateRequest", + "description": "Request payload to update an existing LocationSummary. A summary of a physical business location.", + "type": "object", + "additionalProperties": true + }, + "LookupRequest": { + "type": "object", + "description": "Request body for catalog lookup.", + "required": [ + "ids" + ], + "properties": { + "ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "description": "Identifiers to lookup. Implementations MUST support product ID and variant ID; MAY support secondary identifiers (SKU, handle, etc.)." + }, + "filters": { + "$ref": "#/components/schemas/SearchFilters", + "description": "Filter criteria to narrow returned products and variants. All specified filters combine with AND logic." + }, + "context": { + "$ref": "#/components/schemas/Context" + }, + "signals": { + "$ref": "#/components/schemas/Signals" + }, + "attribution": { + "$ref": "#/components/schemas/Attribution" + } + } + }, + "LookupResponse": { + "type": "object", + "required": [ + "ucp", + "products" + ], + "properties": { + "ucp": { + "$ref": "#/components/schemas/ResponseCatalogSchema" + }, + "products": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/Product" + }, + { + "properties": { + "variants": { + "items": { + "$ref": "#/components/schemas/LookupVariant" + } + } + } + } + ] + }, + "description": "Products matching the requested identifiers. May contain fewer items if some identifiers not found, or more if identifiers match multiple products." + }, + "actions": { + "$ref": "#/components/schemas/Actions", + "description": "Outstanding extension-defined Actions for this catalog lookup response." + }, + "messages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + "description": "Errors, warnings, or informational messages about the requested items." + }, + "policies": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Policy" + }, + "description": "Policies (e.g., return/refund terms) that apply to the products in this response. `applies_to` targets are relative to the response root; when absent or empty, refer to the URLs in `links[]`." + } + } + }, + "LookupVariant": { + "description": "Variant with required correlation metadata for lookup responses.", + "allOf": [ + { + "$ref": "#/components/schemas/Variant" + }, + { + "required": [ + "inputs" + ], + "properties": { + "inputs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InputCorrelation" + }, + "minItems": 1, + "description": "Which request identifiers resolved to this variant, and how. Each entry maps a request ID to its match type." + } + } + } + ] + }, + "MapOrder": { + "type": "object", + "description": "Preferred key order for map-valued fields in the scope annotated by the containing `ucp` member. Each property names a target map, and its array lists target keys in preferred order. Lists may be partial and are not allowlists.", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "Measure": { + "allOf": [ + { + "$ref": "#/components/schemas/Unit" + }, + { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "description": "Integer count of `10^-scale` units of `unit`." + } + } + } + ], + "description": "A measure composed of an integer value and a unit descriptor. Its value is the integer count of `10^-scale` units of `unit`.", + "title": "Measure" + }, + "Media": { + "properties": { + "type": { + "type": "string", + "description": "Media type. Well-known values: `image`, `video`, `model_3d`." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the media resource." + }, + "alt_text": { + "type": "string", + "description": "Accessibility text describing the media." + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels (for images/video)." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels (for images/video)." + } + }, + "required": [ + "type", + "url" + ], + "title": "Media", + "description": "Media item (image, video, etc.).", + "type": "object" + }, + "Members": { + "type": "object", + "description": "Members defined inside the reserved `ucp` protocol object. The object is open for forward compatibility: consumers MUST ignore unrecognized members. Only UCP core defines members, and every defined member MUST be safe to ignore.", + "properties": { + "map_order": { + "$ref": "#/components/schemas/MapOrder" + }, + "request_constraints": { + "$ref": "#/components/schemas/RequestConstraints" + } + }, + "additionalProperties": true + }, + "Message": { + "oneOf": [ + { + "$ref": "#/components/schemas/MessageError" + }, + { + "$ref": "#/components/schemas/MessageWarning" + }, + { + "$ref": "#/components/schemas/MessageInfo" + } + ], + "description": "Container for error, warning, or info messages.", + "title": "Message", + "type": "object", + "discriminator": { + "propertyName": "type", + "mapping": { + "error": "#/components/schemas/MessageError", + "info": "#/components/schemas/MessageInfo", + "warning": "#/components/schemas/MessageWarning" + } + } + }, + "MessageError": { + "properties": { + "type": { + "type": "string", + "const": "error", + "description": "Message type discriminator.", + "default": "error" + }, + "code": { + "$ref": "#/components/schemas/ErrorCode" + }, + "path": { + "type": "string", + "description": "RFC 9535 JSONPath to the component the message refers to (e.g., $.line_items[0])." + }, + "content_type": { + "type": "string", + "enum": [ + "plain", + "markdown" + ], + "default": "plain", + "description": "Content format, default = plain." + }, + "content": { + "type": "string", + "description": "Human-readable message." + }, + "severity": { + "type": "string", + "enum": [ + "recoverable", + "requires_buyer_input", + "requires_buyer_review", + "unrecoverable" + ], + "description": "Reflects the resource state and recommended action. 'recoverable': platform can resolve the condition in band, for example by modifying inputs or processing a related Action, and submit a new operation when needed. 'requires_buyer_input': merchant requires information their API doesn't support collecting programmatically (checkout incomplete). 'requires_buyer_review': buyer must authorize before order placement due to policy, regulatory, or entitlement rules. 'unrecoverable': no valid resource exists to act on, retry with new resource or inputs. Errors with 'requires_*' severity contribute to 'status: requires_escalation'." + } + }, + "required": [ + "type", + "code", + "content", + "severity" + ], + "title": "Message Error", + "type": "object" + }, + "MessageInfo": { + "properties": { + "type": { + "type": "string", + "const": "info", + "description": "Message type discriminator.", + "default": "info" + }, + "path": { + "type": "string", + "description": "RFC 9535 JSONPath to the component the message refers to (e.g., $.line_items[0])." + }, + "code": { + "$ref": "#/components/schemas/InfoCode" + }, + "content_type": { + "type": "string", + "enum": [ + "plain", + "markdown" + ], + "default": "plain", + "description": "Content format, default = plain." + }, + "content": { + "type": "string", + "description": "Human-readable message." + } + }, + "required": [ + "type", + "content" + ], + "title": "Message Info", + "type": "object" + }, + "MessageWarning": { + "properties": { + "type": { + "type": "string", + "const": "warning", + "description": "Message type discriminator.", + "default": "warning" + }, + "path": { + "type": "string", + "description": "RFC 9535 JSONPath to the component the message refers to (e.g., $.line_items[0])." + }, + "code": { + "$ref": "#/components/schemas/WarningCode" + }, + "content": { + "type": "string", + "description": "Human-readable warning message that MUST be displayed." + }, + "content_type": { + "type": "string", + "enum": [ + "plain", + "markdown" + ], + "default": "plain", + "description": "Content format, default = plain." + }, + "presentation": { + "type": "string", + "default": "notice", + "description": "Rendering contract for this warning. 'notice' (default): platform MUST display, MAY dismiss. 'disclosure': platform MUST display in proximity to the path-referenced component, MUST NOT hide or auto-dismiss. See specification for full contract." + }, + "image_url": { + "type": "string", + "format": "uri", + "description": "URL to a required visual element (e.g., warning symbol, energy class label)." + }, + "url": { + "type": "string", + "format": "uri", + "description": "Reference URL for more information (e.g., regulatory site, registry entry, policy page)." + } + }, + "required": [ + "type", + "code", + "content" + ], + "title": "Message Warning", + "type": "object" + }, + "OptionValue": { + "properties": { + "id": { + "type": "string", + "description": "Optional server-assigned identifier for this option value. When present in a selected_option, the server SHOULD use it for matching instead of label." + }, + "label": { + "type": "string", + "description": "Display text for this option value (e.g., 'Small', 'Blue')." + } + }, + "required": [ + "label" + ], + "title": "Option Value", + "description": "A selectable value for a product option.", + "type": "object" + }, + "Order": { + "required": [ + "ucp", + "id", + "checkout_id", + "permalink_url", + "line_items", + "fulfillment", + "currency", + "totals" + ], + "type": "object", + "properties": { + "ucp": { + "$ref": "#/components/schemas/ResponseOrderSchema" + }, + "id": { + "type": "string", + "description": "Unique order identifier." + }, + "label": { + "type": "string", + "description": "Human-readable label for identifying the order. MUST only be provided by the business." + }, + "checkout_id": { + "type": "string", + "description": "Associated checkout ID for reconciliation." + }, + "permalink_url": { + "type": "string", + "format": "uri", + "description": "Permalink to access the order on merchant site." + }, + "line_items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrderLineItem" + }, + "description": "Line items representing what was purchased — can change post-order via edits or exchanges." + }, + "fulfillment": { + "type": "object", + "properties": { + "expectations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Expectation" + }, + "description": "Buyer-facing groups representing when/how items will be delivered. Can be split, merged, or adjusted post-order." + }, + "events": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FulfillmentEvent" + }, + "description": "Append-only event log of actual shipments. Each event references line items by ID." + } + }, + "description": "Fulfillment data: buyer expectations and what actually happened." + }, + "adjustments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Adjustment" + }, + "description": "Post-order events (refunds, returns, credits, disputes, cancellations, etc.) that exist independently of fulfillment." + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code. MUST match the currency from the originating checkout session." + }, + "totals": { + "$ref": "#/components/schemas/Totals", + "description": "Different totals for the order." + }, + "policies": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Policy" + }, + "description": "Snapshot of the policies that applied to the items at checkout, captured on the order as a durable record. `applies_to` targets are relative to the response root." + }, + "messages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + "description": "Business outcome messages (errors, warnings, informational). Present when the business needs to communicate status or issues to the platform." + }, + "attribution": { + "$ref": "#/components/schemas/Attribution", + "description": "Snapshot of the attribution associated with the originating checkout. Read-only on the order." + } + }, + "title": "Order", + "description": "Order schema with line items, buyer-facing fulfillment expectations, and event logs." + }, + "OrderConfirmation": { + "properties": { + "id": { + "type": "string", + "description": "Unique order identifier." + }, + "label": { + "type": "string", + "description": "Human-readable label for identifying the order. MUST only be provided by the business." + }, + "permalink_url": { + "type": "string", + "format": "uri", + "description": "Permalink to access the order on merchant site." + } + }, + "required": [ + "id", + "permalink_url" + ], + "title": "Order Confirmation", + "description": "Order details available at the time of checkout completion.", + "type": "object" + }, + "OrderCreateRequest": { + "required": [ + "ucp", + "id", + "checkout_id", + "permalink_url", + "line_items", + "fulfillment", + "totals" + ], + "type": "object", + "properties": { + "ucp": { + "$ref": "#/components/schemas/ResponseOrderSchema" + }, + "id": { + "type": "string", + "description": "Unique order identifier." + }, + "label": { + "type": "string", + "description": "Human-readable label for identifying the order. MUST only be provided by the business." + }, + "checkout_id": { + "type": "string", + "description": "Associated checkout ID for reconciliation." + }, + "permalink_url": { + "type": "string", + "format": "uri", + "description": "Permalink to access the order on merchant site." + }, + "line_items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrderLineItem" + }, + "description": "Line items representing what was purchased — can change post-order via edits or exchanges." + }, + "fulfillment": { + "type": "object", + "properties": { + "expectations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Expectation" + }, + "description": "Buyer-facing groups representing when/how items will be delivered. Can be split, merged, or adjusted post-order." + }, + "events": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FulfillmentEvent" + }, + "description": "Append-only event log of actual shipments. Each event references line items by ID." + } + }, + "description": "Fulfillment data: buyer expectations and what actually happened." + }, + "adjustments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Adjustment" + }, + "description": "Post-order events (refunds, returns, credits, disputes, cancellations, etc.) that exist independently of fulfillment." + }, + "totals": { + "$ref": "#/components/schemas/TotalsCreateRequest", + "description": "Different totals for the order." + }, + "messages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + "description": "Business outcome messages (errors, warnings, informational). Present when the business needs to communicate status or issues to the platform." + } + }, + "title": "OrderCreateRequest", + "description": "Request payload to create a new Order. Order schema with line items, buyer-facing fulfillment expectations, and event logs." + }, + "OrderLineItem": { + "properties": { + "id": { + "type": "string", + "description": "Line item identifier." + }, + "item": { + "$ref": "#/components/schemas/Item", + "description": "Purchased item data, including identity, price, and sale basis." + }, + "quantity": { + "type": "object", + "required": [ + "total", + "fulfilled" + ], + "properties": { + "original": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991, + "description": "Quantity from the original checkout, expressed as an integer step count." + }, + "total": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991, + "description": "Current active quantity after returns, cancellations, or other order changes, expressed as an integer step count." + }, + "fulfilled": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991, + "description": "Quantity fulfilled so far, expressed as an integer step count." + } + }, + "description": "Tracks the line item's original, current active, and fulfilled quantities. All three values use the same inherited `item.quantity_unit`. When `item.quantity_unit` is absent on an authoritative order response, each step is one whole item (`each`) under the shared default." + }, + "totals": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Total" + }, + "description": "Line item totals breakdown." + }, + "status": { + "type": "string", + "enum": [ + "processing", + "partial", + "fulfilled", + "removed" + ], + "description": "Derived status: removed if quantity.total == 0, fulfilled if quantity.total > 0 and quantity.fulfilled == quantity.total, partial if quantity.total > 0 and quantity.fulfilled > 0, otherwise processing." + }, + "parent_id": { + "type": "string", + "description": "Parent line item identifier for any nested structures." + } + }, + "required": [ + "id", + "item", + "quantity", + "totals", + "status" + ], + "title": "Order Line Item", + "type": "object" + }, + "OrderPlatformSchema": { + "title": "Platform Order Schema", + "description": "Platform's order capability configuration.", + "type": "object", + "required": [ + "webhook_url" + ], + "properties": { + "webhook_url": { + "type": "string", + "format": "uri", + "description": "URL where merchant sends order lifecycle events (webhooks)." + } + } + }, + "OrderUpdateRequest": { + "required": [ + "ucp", + "id", + "checkout_id", + "permalink_url", + "line_items", + "fulfillment", + "totals" + ], + "type": "object", + "properties": { + "ucp": { + "$ref": "#/components/schemas/ResponseOrderSchema" + }, + "id": { + "type": "string", + "description": "Unique order identifier." + }, + "label": { + "type": "string", + "description": "Human-readable label for identifying the order. MUST only be provided by the business." + }, + "checkout_id": { + "type": "string", + "description": "Associated checkout ID for reconciliation." + }, + "permalink_url": { + "type": "string", + "format": "uri", + "description": "Permalink to access the order on merchant site." + }, + "line_items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrderLineItem" + }, + "description": "Line items representing what was purchased — can change post-order via edits or exchanges." + }, + "fulfillment": { + "type": "object", + "properties": { + "expectations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Expectation" + }, + "description": "Buyer-facing groups representing when/how items will be delivered. Can be split, merged, or adjusted post-order." + }, + "events": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FulfillmentEvent" + }, + "description": "Append-only event log of actual shipments. Each event references line items by ID." + } + }, + "description": "Fulfillment data: buyer expectations and what actually happened." + }, + "adjustments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Adjustment" + }, + "description": "Post-order events (refunds, returns, credits, disputes, cancellations, etc.) that exist independently of fulfillment." + }, + "totals": { + "$ref": "#/components/schemas/TotalsUpdateRequest", + "description": "Different totals for the order." + }, + "messages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + "description": "Business outcome messages (errors, warnings, informational). Present when the business needs to communicate status or issues to the platform." + } + }, + "title": "OrderUpdateRequest", + "description": "Request payload to update an existing Order. Order schema with line items, buyer-facing fulfillment expectations, and event logs." + }, + "Payment": { + "properties": { + "instruments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SelectedPaymentInstrument" + }, + "description": "The payment instruments available for this payment. Each instrument is associated with a specific handler via the handler_id field. Handlers can extend the base payment_instrument schema to add handler-specific fields." + } + }, + "type": "object", + "title": "Payment", + "description": "Payment configuration containing handlers." + }, + "PaymentCredential": { + "additionalProperties": true, + "properties": { + "type": { + "type": "string", + "description": "The credential type discriminator. Specific schemas will constrain this to a constant value." + } + }, + "title": "Payment Credential", + "description": "The base definition for any payment credential. Handlers define specific credential types.", + "type": "object", + "required": [ + "type" + ] + }, + "PaymentHandlerBase": { + "allOf": [ + { + "$ref": "#/components/schemas/UcpEntity" + }, + { + "type": "object", + "required": [ + "id" + ] + }, + { + "type": "object", + "properties": { + "available_instruments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AvailablePaymentInstrument" + }, + "description": "Instrument types this handler supports, with optional constraints. When absent, every instrument should be considered available.", + "minItems": 1 + } + } + } + ] + }, + "PaymentHandlerBusinessSchema": { + "title": "Payment Handler (Business Schema)", + "description": "Business declaration for discovery profiles. May include partial config state required for discovery.", + "allOf": [ + { + "$ref": "#/components/schemas/PaymentHandlerBase" + } + ] + }, + "PaymentHandlerPlatformSchema": { + "title": "Payment Handler (Platform Schema)", + "description": "Platform declaration for discovery profiles. May include partial config state required for discovery.", + "allOf": [ + { + "$ref": "#/components/schemas/PaymentHandlerBase" + }, + { + "required": [ + "spec", + "schema" + ] + } + ] + }, + "PaymentHandlerResponseSchema": { + "title": "Payment Handler (Response Schema)", + "description": "Handler reference in responses. May include full config state for runtime usage of the handler.", + "allOf": [ + { + "$ref": "#/components/schemas/PaymentHandlerBase" + } + ] + }, + "PaymentInstrument": { + "additionalProperties": true, + "properties": { + "id": { + "type": "string", + "description": "A unique identifier for this instrument instance. Typically assigned by the platform for instruments it collects. For a business-owned saved instrument returned on an identity-linked response, this identifier is assigned by the business; the platform MUST treat it as an opaque, business-scoped reference, and the business resolves it server-side when the buyer selects it." + }, + "handler_id": { + "type": "string", + "description": "The unique identifier for the handler instance that produced this instrument. This corresponds to the 'id' field in the Payment Handler definition." + }, + "type": { + "type": "string", + "description": "The broad category of the instrument (e.g., 'card', 'tokenized_card'). Specific schemas will constrain this to a constant value." + }, + "billing_address": { + "$ref": "#/components/schemas/PostalAddress", + "description": "The billing address associated with this payment method." + }, + "credential": { + "$ref": "#/components/schemas/PaymentCredential" + }, + "display": { + "type": "object", + "description": "Display information for this payment instrument. Each payment instrument schema defines its specific display properties, as outlined by the payment handler." + } + }, + "title": "Payment Instrument", + "description": "The base definition for any payment instrument. It links the instrument to a specific payment handler.", + "type": "object", + "required": [ + "id", + "handler_id", + "type" + ] + }, + "PlatformFulfillmentConfig": { + "properties": { + "supports_multi_group": { + "type": "boolean", + "default": false, + "description": "Enables multiple groups per method." + } + }, + "type": "object", + "title": "Platform Fulfillment Config", + "description": "Platform's fulfillment configuration." + }, + "Policy": { + "properties": { + "type": { + "$ref": "#/components/schemas/ReverseDomainName", + "description": "Policy type discriminator. Open reverse-DNS vocabulary. Well-known values: `dev.ucp.shopping.policy.return` (return terms), `dev.ucp.shopping.policy.warranty` (warranty terms). Businesses MAY define custom types in their own domain (e.g., `com.example.policy.price_match`). Platforms MUST tolerate unknown values." + }, + "description": { + "$ref": "#/components/schemas/Description", + "description": "Human-readable policy summary in one or more formats (plain, markdown, html). Required on every policy so a platform can present it without understanding any type-specific fields. This is not the buyer-facing disclosure — display is compelled by a `messages[]` warning (see the Policies section)." + }, + "applies_to": { + "type": "array", + "items": { + "type": "string" + }, + "description": "RFC 9535 JSONPath expressions identifying the nodes this policy applies to, relative to the embedding response root (e.g., `$.line_items[0]` in cart/checkout, `$.products[2]` in catalog). Each target covers the node it names and everything nested under it, so a target on a product also covers its variants. A singular query (RFC 9535 Section 2.3.5.1; name and index selectors only) names a single node; filters, wildcards, and slices match a set. When omitted, the policy applies to the entire response. When policies of the same `type` contest a node, the narrowest target wins and overrides the rest. See the Policies section for how specificity resolves." + }, + "url": { + "type": "string", + "format": "uri", + "description": "Optional link to the full policy document." + } + }, + "additionalProperties": true, + "title": "Policy", + "description": "A durable business rule about the items in a response — return/refund terms, warranty, and the like — at the time of purchase. Every policy carries a `type` (an open reverse-DNS vocabulary) and a `description` so a platform can present it without understanding its type-specific fields; type-specific fields (gated by `type`) add structured context for platforms that model that type. Policies are reference data; the obligation to display a term to the buyer is carried by a `messages[]` warning whose `code` equals the policy `type` — see the Policies section of the specification.", + "type": "object", + "required": [ + "type", + "description" + ] + }, + "PostalAddress": { + "properties": { + "extended_address": { + "type": "string", + "description": "An address extension such as an apartment number, C/O or alternative name." + }, + "street_address": { + "type": "string", + "description": "The street address." + }, + "address_locality": { + "type": "string", + "description": "The locality in which the street address is, and which is in the region. For example, Mountain View." + }, + "address_region": { + "type": "string", + "description": "The region in which the locality is, and which is in the country. Required for applicable countries (i.e. state in US, province in CA). For example, California or another appropriate first-level Administrative division." + }, + "address_country": { + "type": "string", + "description": "The country. Recommended to be in 2-letter ISO 3166-1 alpha-2 format, for example \"US\". For backward compatibility, a 3-letter ISO 3166-1 alpha-3 country code such as \"SGP\" or a full country name such as \"Singapore\" can also be used." + }, + "postal_code": { + "type": "string", + "description": "The postal code. For example, 94043." + }, + "first_name": { + "type": "string", + "description": "Optional. First name of the contact associated with the address." + }, + "last_name": { + "type": "string", + "description": "Optional. Last name of the contact associated with the address." + }, + "phone_number": { + "type": "string", + "description": "Optional. Phone number of the contact associated with the address." + } + }, + "type": "object", + "title": "Postal Address" + }, + "Price": { + "properties": { + "amount": { + "$ref": "#/components/schemas/Amount", + "description": "Amount in ISO 4217 minor units. Use 0 for free items." + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code (e.g., 'USD', 'EUR', 'GBP').", + "pattern": "^[A-Z]{3}$" + } + }, + "required": [ + "amount", + "currency" + ], + "title": "Price", + "description": "Price with explicit currency.", + "type": "object" + }, + "PriceFilter": { + "properties": { + "min": { + "$ref": "#/components/schemas/Amount", + "description": "Minimum price in ISO 4217 minor units." + }, + "max": { + "$ref": "#/components/schemas/Amount", + "description": "Maximum price in ISO 4217 minor units." + } + }, + "type": "object", + "title": "Price Filter", + "description": "Price range filter denominated in context.currency. When context.currency matches the presentment currency, businesses apply the filter directly. When it differs, businesses SHOULD convert filter values to the presentment currency before applying; if conversion is not supported, businesses MAY ignore the filter and SHOULD indicate this via a message. When context.currency is absent, filter denomination is ambiguous and businesses MAY ignore it." + }, + "PriceRange": { + "properties": { + "min": { + "$ref": "#/components/schemas/Price", + "description": "Minimum price in the range." + }, + "max": { + "$ref": "#/components/schemas/Price", + "description": "Maximum price in the range." + } + }, + "required": [ + "min", + "max" + ], + "title": "Price Range", + "description": "A price range representing minimum and maximum values (e.g., a common example in retail shopping is when prices vary across product variants).", + "type": "object" + }, + "Product": { + "properties": { + "id": { + "type": "string", + "description": "Global ID (GID) uniquely identifying this product." + }, + "handle": { + "type": "string", + "description": "URL-safe slug for SEO-friendly URLs (e.g., 'blue-runner-pro'). Use id for stable API references." + }, + "title": { + "type": "string", + "description": "Product title." + }, + "description": { + "$ref": "#/components/schemas/Description", + "description": "Product description in one or more formats." + }, + "url": { + "type": "string", + "format": "uri", + "description": "Canonical product page URL." + }, + "categories": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Category" + }, + "description": "Product categories with optional taxonomy identifiers." + }, + "price_range": { + "$ref": "#/components/schemas/PriceRange", + "description": "Price range across all variants." + }, + "list_price_range": { + "$ref": "#/components/schemas/PriceRange", + "description": "List price range before discounts (for strikethrough display)." + }, + "media": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Media" + }, + "description": "Product media (images, videos, 3D models). First item is the featured media for listings." + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProductOption" + }, + "description": "Product options (Size, Color, etc.)." + }, + "variants": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Variant" + }, + "minItems": 1, + "description": "Purchasable variants of this product. First item is the featured variant for listings." + }, + "rating": { + "$ref": "#/components/schemas/Rating", + "description": "Aggregate product rating." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Product tags for categorization and search." + }, + "metadata": { + "type": "object", + "description": "Business-defined custom data extending the standard product model." + } + }, + "required": [ + "id", + "title", + "description", + "price_range", + "variants" + ], + "title": "Product", + "description": "A product in the catalog with variants and options.", + "type": "object" + }, + "ProductOption": { + "properties": { + "name": { + "type": "string", + "description": "Option name (e.g., 'Size', 'Color')." + }, + "values": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OptionValue" + }, + "minItems": 1, + "description": "Available values for this option." + } + }, + "required": [ + "name", + "values" + ], + "title": "Product Option", + "description": "A product option such as size, color, or material.", + "type": "object" + }, + "Profile": { + "allOf": [ + { + "$ref": "#/components/schemas/ProfileBase" + } + ], + "description": "Variant-neutral wrapper schema for UCP profile documents. Use the business_schema definition to validate business profiles and the platform_schema definition to validate platform profiles.", + "title": "UCP Profile Document" + }, + "ProfileBase": { + "type": "object", + "description": "Common wrapper for UCP profile documents.", + "required": [ + "ucp" + ], + "properties": { + "ucp": { + "$ref": "#/components/schemas/UcpBase", + "description": "Protocol metadata, capabilities, services, and payment handlers advertised by this party." + }, + "keys": { + "type": "array", + "description": "Canonical UCP profile field for publishing signing keys, as a JWK Set per RFC 7517. When a profile publishes signing keys, they MUST appear here; this is where every UCP verifier reads them. Publishing keys[] makes the UCP profile a valid JWK Set that a signer can reuse as its Web Bot Auth key source: a WBA-shape verifier resolving via Signature-Agent type=jwks_uri pointed at this profile reads these keys, and the cimd and directory variants reach them through their own documents. See the Deployment Patterns for WBA Interop section in the overview for hosting patterns.", + "items": { + "$ref": "#/components/schemas/JwkPublicKey" + } + } + }, + "additionalProperties": true + }, + "ProfileBusinessSchema": { + "title": "UCP Business Profile Document", + "description": "Profile document hosted by a business at /.well-known/ucp.", + "allOf": [ + { + "$ref": "#/components/schemas/ProfileBase" + }, + { + "type": "object", + "properties": { + "ucp": { + "$ref": "#/components/schemas/UcpBusinessSchema" + } + } + } + ] + }, + "ProfilePlatformSchema": { + "title": "UCP Platform Profile Document", + "description": "Profile document hosted by a platform and advertised to businesses via UCP-Agent.", + "allOf": [ + { + "$ref": "#/components/schemas/ProfileBase" + }, + { + "type": "object", + "properties": { + "ucp": { + "$ref": "#/components/schemas/UcpPlatformSchema" + } + } + } + ] + }, + "QuantityUnit": { + "allOf": [ + { + "$ref": "#/components/schemas/Unit" + }, + { + "type": "object", + "properties": { + "increment": { + "type": "integer", + "minimum": 1, + "default": 1, + "description": "Ordering granularity, denominated in steps: the Business sells this item in integer multiples of `increment` steps. Its effective value is the provided value or 1. Advisory merchandising policy, not a representational bound: Platform-authored quantities SHOULD be integer multiples of the effective increment; the Business MAY accept, revise, or reject an off-increment request with a recoverable business outcome and MUST NOT silently reinterpret it. Business-authored quantities (checkout revisions, fulfillment events, adjustments) are bounded only by `scale`." + } + } + } + ], + "description": "Sale-basis descriptor for quantities: the shared unit descriptor plus the Business's ordering policy. Its unit-descriptor machine identity remains (`unit`, effective `scale`); `display_text` and `increment` are excluded from identity and mismatch comparison.", + "title": "Quantity Unit" + }, + "Rating": { + "properties": { + "value": { + "type": "number", + "minimum": 0, + "description": "Average rating value." + }, + "scale_min": { + "type": "number", + "minimum": 0, + "default": 1, + "description": "Minimum value on the rating scale (e.g., 1 for 1-5 stars)." + }, + "scale_max": { + "type": "number", + "minimum": 1, + "description": "Maximum value on the rating scale (e.g., 5 for 5-star)." + }, + "count": { + "type": "integer", + "minimum": 0, + "description": "Number of reviews contributing to the rating." + } + }, + "required": [ + "value", + "scale_max" + ], + "title": "Rating", + "description": "Product rating aggregate.", + "type": "object" + }, + "Request": { + "type": "object", + "description": "Pagination parameters for requests.", + "properties": { + "cursor": { + "type": "string", + "description": "Opaque cursor from previous response." + }, + "limit": { + "type": "integer", + "minimum": 1, + "description": "Requested page size, not a guaranteed result count. When omitted, the Business MUST apply a default page size. A default of 10 is RECOMMENDED, but the Business MAY choose another value. The Business MAY return fewer results than the requested or default page size, including when enforcing its maximum page size. A Platform MUST NOT assume that the response count equals either value." + } + } + }, + "RequestConstraints": { + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "description": "A complete RFC 9535 JSONPath query evaluated against the next logical UCP request to the same resource." + }, + "required": { + "type": "array", + "description": "Property names required by the constrained object. Must be non-empty: an empty array applies no constraint.", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "properties": { + "type": "object", + "description": "Constraints keyed by property name. Must be non-empty: an empty object applies no constraint.", + "minProperties": 1, + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/components/schemas/ConstraintExpression" + }, + { + "$ref": "#/components/schemas/ValueConstraint" + } + ] + } + }, + "anyOf": { + "type": "array", + "description": "Alternative Object Constraints. The constrained object must satisfy at least one. A branch must be non-empty: an empty branch is satisfied by every object and neutralizes the alternation.", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/ConstraintExpression", + "minProperties": 1 + } + } + }, + "title": "Request Constraints", + "description": "Binds the shared Constraint Expression grammar to data in the next UCP request to the same resource.", + "type": "object" + }, + "Requires": { + "type": "object", + "description": "Version requirements for extension schemas. Declares minimum (and optionally maximum) protocol and capability versions needed for correct operation.", + "properties": { + "protocol": { + "$ref": "#/components/schemas/VersionConstraint", + "description": "Required range for the selected `ucp.version`." + }, + "capabilities": { + "type": "object", + "description": "Required capability versions, keyed by capability name. Keys must be a subset of the extension's $defs keys.", + "propertyNames": { + "$ref": "#/components/schemas/ReverseDomainName" + }, + "additionalProperties": { + "$ref": "#/components/schemas/VersionConstraint" + } + } + }, + "additionalProperties": true + }, + "Response": { + "type": "object", + "description": "Pagination information in responses.", + "properties": { + "cursor": { + "type": "string", + "description": "Cursor to fetch the next page of results. MUST be present when has_next_page is true." + }, + "has_next_page": { + "type": "boolean", + "description": "Whether more results are available." + }, + "total_count": { + "type": "integer", + "minimum": 0, + "description": "Total number of matching items, if available." + } + }, + "required": [ + "has_next_page" + ], + "if": { + "properties": { + "has_next_page": { + "const": true, + "default": true, + "type": "boolean" + } + }, + "required": [ + "has_next_page" + ] + }, + "then": { + "required": [ + "cursor" + ] + } + }, + "ResponseCartSchema": { + "title": "UCP Cart Response Schema", + "description": "UCP metadata for cart responses. No payment handlers needed pre-checkout.", + "allOf": [ + { + "$ref": "#/components/schemas/UcpBase" + }, + { + "properties": { + "capabilities": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/CapabilityResponseSchema" + } + } + } + } + } + ] + }, + "ResponseCatalogSchema": { + "title": "UCP Catalog Response Schema", + "description": "UCP metadata for catalog responses.", + "allOf": [ + { + "$ref": "#/components/schemas/UcpBase" + }, + { + "properties": { + "capabilities": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/CapabilityResponseSchema" + } + } + } + } + } + ] + }, + "ResponseCheckoutSchema": { + "title": "UCP Checkout Response Schema", + "description": "UCP metadata for checkout responses.", + "allOf": [ + { + "$ref": "#/components/schemas/UcpBase" + }, + { + "required": [ + "payment_handlers" + ], + "properties": { + "services": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/ServiceResponseSchema" + } + } + }, + "capabilities": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/CapabilityResponseSchema" + } + } + }, + "payment_handlers": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/PaymentHandlerResponseSchema" + } + } + } + } + } + ] + }, + "ResponseLocationSchema": { + "title": "UCP Location Response Schema", + "description": "UCP metadata for location responses.", + "allOf": [ + { + "$ref": "#/components/schemas/UcpBase" + }, + { + "properties": { + "capabilities": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/CapabilityResponseSchema" + } + } + } + } + } + ] + }, + "ResponseOrderSchema": { + "title": "UCP Order Response Schema", + "description": "UCP metadata for order responses. No payment handlers needed post-purchase.", + "allOf": [ + { + "$ref": "#/components/schemas/UcpBase" + }, + { + "properties": { + "capabilities": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/CapabilityResponseSchema" + } + } + } + } + } + ] + }, + "ReverseDomainName": { + "examples": [ + "dev.ucp.shopping.checkout", + "dev.ucp.common.identity_linking", + "com.example.loyalty_gold", + "com.example-shop.checkout", + "com.2example.cart", + "uk.co.example-shop.checkout", + "xn--p1ai.example.checkout" + ], + "pattern": "^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?)+$", + "title": "Reverse Domain Name", + "description": "Reverse-domain identifier used for collision-safe namespacing of capabilities, services, handlers, eligibility claims, and extension-contributed keys. Must contain at least two dot-separated segments (e.g., 'dev.ucp.shopping.checkout', 'com.example.loyalty_gold'). Segments after the first are domain- or identifier-derived: they may contain interior hyphens, may start with a digit, and may contain underscores (e.g., 'com.example-shop.checkout', 'com.2example.cart', 'dev.ucp.common.identity_linking'), but must not start or end with a hyphen. The first segment (the reversed top-level domain) is letters and digits, and may contain interior hyphens to support internationalized (punycode) top-level domains such as 'xn--p1ai'.", + "type": "string" + }, + "SearchFilters": { + "additionalProperties": true, + "properties": { + "categories": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Filter by product categories (OR logic — matches products in any listed categories). Values match against the value field in product category entries. Valid values can be discovered from the categories field in search results, merchant documentation, or standard taxonomies that businesses may align with." + }, + "price": { + "$ref": "#/components/schemas/PriceFilter" + } + }, + "title": "Search Filters", + "description": "Filter criteria to narrow search results. All specified filters combine with AND logic.", + "type": "object" + }, + "SearchRequest": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Free-text search query." + }, + "context": { + "$ref": "#/components/schemas/Context" + }, + "signals": { + "$ref": "#/components/schemas/Signals" + }, + "attribution": { + "$ref": "#/components/schemas/Attribution" + }, + "filters": { + "$ref": "#/components/schemas/SearchFilters" + }, + "pagination": { + "$ref": "#/components/schemas/Request" + } + } + }, + "SearchResponse": { + "type": "object", + "required": [ + "ucp", + "products" + ], + "properties": { + "ucp": { + "$ref": "#/components/schemas/ResponseCatalogSchema" + }, + "products": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + }, + "description": "Products matching the search criteria." + }, + "pagination": { + "$ref": "#/components/schemas/Response" + }, + "actions": { + "$ref": "#/components/schemas/Actions", + "description": "Outstanding extension-defined Actions for this catalog search response." + }, + "messages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + "description": "Errors, warnings, or informational messages about the search results." + }, + "policies": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Policy" + }, + "description": "Policies (e.g., return/refund terms) that apply to the products in these search results. `applies_to` targets are relative to the response root; when absent or empty, refer to the URLs in `links[]`." + } + } + }, + "SelectedOption": { + "properties": { + "name": { + "type": "string", + "description": "Option name (e.g., 'Size')." + }, + "id": { + "type": "string", + "description": "Optional option value identifier from option_value.id. When present, the server SHOULD use it for matching; name and label remain required for display." + }, + "label": { + "type": "string", + "description": "Selected option label (e.g., 'Large')." + } + }, + "required": [ + "name", + "label" + ], + "title": "Selected Option", + "description": "A specific option selection on a variant (e.g., Size: Large).", + "type": "object" + }, + "SelectedPaymentInstrument": { + "title": "Selected Payment Instrument", + "description": "A payment instrument with selection state.", + "allOf": [ + { + "$ref": "#/components/schemas/PaymentInstrument" + }, + { + "type": "object", + "properties": { + "selected": { + "type": "boolean", + "description": "Whether this instrument is selected by the user." + } + } + } + ] + }, + "ServiceBase": { + "allOf": [ + { + "$ref": "#/components/schemas/UcpEntity" + }, + { + "type": "object", + "required": [ + "transport" + ], + "properties": { + "transport": { + "type": "string", + "enum": [ + "rest", + "mcp", + "a2a", + "embedded" + ], + "description": "Transport protocol for this service binding." + }, + "endpoint": { + "type": "string", + "format": "uri", + "description": "Endpoint URL for this transport binding." + } + } + } + ] + }, + "ServiceBusinessSchema": { + "title": "Service (Business Schema)", + "description": "Service binding for business/merchant configuration. May override platform endpoints.", + "allOf": [ + { + "$ref": "#/components/schemas/ServiceBase" + }, + { + "anyOf": [ + { + "properties": { + "transport": { + "const": "rest", + "default": "rest", + "type": "string" + } + } + }, + { + "properties": { + "transport": { + "const": "mcp", + "default": "mcp", + "type": "string" + } + } + }, + { + "properties": { + "transport": { + "const": "a2a", + "default": "a2a", + "type": "string" + } + } + }, + { + "properties": { + "transport": { + "const": "embedded", + "default": "embedded", + "type": "string" + }, + "config": { + "$ref": "#/components/schemas/EmbeddedConfig" + } + } + } + ] + } + ] + }, + "ServicePlatformSchema": { + "title": "Service (Platform Schema)", + "description": "Full service declaration for platform-level discovery. All transports require `version`, `spec`, and `transport`. REST, MCP, and embedded additionally require `schema`.", + "allOf": [ + { + "$ref": "#/components/schemas/ServiceBase" + }, + { + "required": [ + "spec" + ] + }, + { + "anyOf": [ + { + "properties": { + "transport": { + "const": "rest", + "default": "rest", + "type": "string" + } + } + }, + { + "properties": { + "transport": { + "const": "mcp", + "default": "mcp", + "type": "string" + } + } + }, + { + "properties": { + "transport": { + "const": "a2a", + "default": "a2a", + "type": "string" + } + } + }, + { + "properties": { + "transport": { + "const": "embedded", + "default": "embedded", + "type": "string" + } + } + } + ] + } + ] + }, + "ServiceResponseSchema": { + "title": "Service (Response Schema)", + "description": "Service binding in API responses. Includes per-resource transport configuration via typed config.", + "allOf": [ + { + "$ref": "#/components/schemas/ServiceBase" + }, + { + "anyOf": [ + { + "properties": { + "transport": { + "const": "rest", + "default": "rest", + "type": "string" + } + } + }, + { + "properties": { + "transport": { + "const": "mcp", + "default": "mcp", + "type": "string" + } + } + }, + { + "properties": { + "transport": { + "const": "a2a", + "default": "a2a", + "type": "string" + } + } + }, + { + "properties": { + "transport": { + "const": "embedded", + "default": "embedded", + "type": "string" + }, + "config": { + "$ref": "#/components/schemas/EmbeddedConfig" + } + } + } + ] + } + ] + }, + "ShippingDestination": { + "allOf": [ + { + "$ref": "#/components/schemas/PostalAddress" + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "ID specific to this shipping destination." + }, + "type": { + "type": "string", + "const": "shipping_address", + "description": "Destination type discriminator.", + "default": "shipping_address" + } + }, + "required": [ + "id", + "type" + ] + } + ], + "type": "object", + "title": "Shipping Destination", + "description": "Shipping destination." + }, + "ShippingDestinationCreateRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/PostalAddress" + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "ID specific to this shipping destination." + }, + "type": { + "type": "string", + "const": "shipping_address", + "description": "Destination type discriminator.", + "default": "shipping_address" + } + } + } + ], + "type": "object", + "title": "ShippingDestinationCreateRequest", + "description": "Request payload to create a new ShippingDestination. Shipping destination." + }, + "ShippingDestinationUpdateRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/PostalAddress" + }, + { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "ID specific to this shipping destination." + }, + "type": { + "type": "string", + "const": "shipping_address", + "description": "Destination type discriminator.", + "default": "shipping_address" + } + } + } + ], + "type": "object", + "title": "ShippingDestinationUpdateRequest", + "description": "Request payload to update an existing ShippingDestination. Shipping destination." + }, + "Signals": { + "additionalProperties": true, + "properties": { + "dev.ucp.buyer_ip": { + "type": "string", + "description": "Client's IP address (IPv4 or IPv6)." + }, + "dev.ucp.user_agent": { + "type": "string", + "description": "Client's HTTP User-Agent header or equivalent." + } + }, + "title": "Signals", + "description": "Environment data provided by the platform to support authorization and abuse prevention. Values MUST NOT be buyer-asserted claims — platforms provide signals based on direct observation or independently verifiable third-party attestations. All signal keys MUST use reverse-domain naming to ensure provenance and prevent collisions when multiple extensions contribute to the shared namespace.", + "type": "object", + "propertyNames": { + "$ref": "#/components/schemas/ReverseDomainName", + "description": "Reverse-domain identifier (e.g., dev.ucp.buyer_ip, com.example.device_id)." + } + }, + "SignedAmount": { + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "title": "Signed Amount", + "description": "Monetary amount in the currency's minor unit as defined by ISO 4217. Refer to the currency's exponent to determine minor-to-major ratio (e.g., 2 for USD, 0 for JPY, 3 for KWD). May be negative — the sign is intrinsic to the value (e.g., discounts are negative, charges are positive).", + "type": "integer" + }, + "Success": { + "description": "UCP metadata with status 'success'. Use for response branches that carry the expected payload.", + "allOf": [ + { + "$ref": "#/components/schemas/UcpBase" + }, + { + "properties": { + "status": { + "const": "success", + "default": "success", + "type": "string" + } + }, + "required": [ + "status" + ] + } + ] + }, + "Total": { + "required": [ + "type", + "amount" + ], + "allOf": [ + { + "if": { + "properties": { + "type": { + "enum": [ + "discount", + "items_discount" + ] + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "amount": { + "exclusiveMaximum": 0 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "enum": [ + "subtotal", + "fulfillment", + "tax", + "fee" + ] + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "amount": { + "minimum": 0 + } + } + } + } + ], + "title": "Total", + "description": "A cost breakdown entry with a category, amount, and optional display text.", + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Cost category. Well-known values: subtotal, items_discount, discount, fulfillment, tax, fee, total. Businesses MAY use additional values." + }, + "display_text": { + "type": "string", + "description": "Text to display against the amount. Should reflect appropriate method (e.g., 'Shipping', 'Delivery')." + }, + "amount": { + "$ref": "#/components/schemas/SignedAmount" + } + } + }, + "TotalCreateRequest": { + "properties": {}, + "allOf": [ + { + "if": { + "properties": { + "type": { + "enum": [ + "discount", + "items_discount" + ] + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "amount": { + "exclusiveMaximum": 0 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "enum": [ + "subtotal", + "fulfillment", + "tax", + "fee" + ] + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "amount": { + "minimum": 0 + } + } + } + } + ], + "title": "TotalCreateRequest", + "description": "Request payload to create a new Total. A cost breakdown entry with a category, amount, and optional display text.", + "type": "object" + }, + "TotalUpdateRequest": { + "properties": {}, + "allOf": [ + { + "if": { + "properties": { + "type": { + "enum": [ + "discount", + "items_discount" + ] + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "amount": { + "exclusiveMaximum": 0 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "enum": [ + "subtotal", + "fulfillment", + "tax", + "fee" + ] + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "amount": { + "minimum": 0 + } + } + } + } + ], + "title": "TotalUpdateRequest", + "description": "Request payload to update an existing Total. A cost breakdown entry with a category, amount, and optional display text.", + "type": "object" + }, + "Totals": { + "allOf": [ + { + "contains": { + "properties": { + "type": { + "const": "subtotal", + "default": "subtotal", + "type": "string" + } + }, + "required": [ + "type" + ] + }, + "minContains": 1, + "maxContains": 1 + }, + { + "contains": { + "properties": { + "type": { + "const": "total", + "default": "total", + "type": "string" + } + }, + "required": [ + "type" + ] + }, + "minContains": 1, + "maxContains": 1 + } + ], + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/Total" + }, + { + "type": "object", + "properties": { + "lines": { + "type": "array", + "items": { + "type": "object", + "properties": { + "display_text": { + "type": "string", + "description": "Human-readable label for this sub-line." + }, + "amount": { + "$ref": "#/components/schemas/SignedAmount" + } + }, + "description": "Sub-line entry. Additional metadata MAY be included.", + "required": [ + "display_text", + "amount" + ] + }, + "description": "Optional itemized breakdown. The parent entry is always rendered; lines are supplementary. Sum of line amounts MUST equal the parent entry amount." + } + } + }, + { + "if": { + "properties": { + "type": { + "not": { + "enum": [ + "subtotal", + "items_discount", + "discount", + "fulfillment", + "tax", + "fee", + "total" + ] + } + } + }, + "required": [ + "type" + ] + }, + "then": { + "required": [ + "display_text" + ] + } + } + ] + }, + "title": "Totals", + "description": "Pricing breakdown provided by the business. MUST contain exactly one subtotal and one total entry. Detail types (tax, fee, discount, fulfillment) may appear multiple times for itemization. Platforms MUST render all entries in order using display_text and amount.", + "type": "array" + }, + "TotalsCreateRequest": { + "allOf": [ + { + "contains": { + "properties": { + "type": { + "const": "subtotal", + "default": "subtotal", + "type": "string" + } + }, + "required": [ + "type" + ] + }, + "minContains": 1, + "maxContains": 1 + }, + { + "contains": { + "properties": { + "type": { + "const": "total", + "default": "total", + "type": "string" + } + }, + "required": [ + "type" + ] + }, + "minContains": 1, + "maxContains": 1 + } + ], + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/TotalCreateRequest" + }, + { + "type": "object", + "properties": {} + }, + { + "if": { + "properties": { + "type": { + "not": { + "enum": [ + "subtotal", + "items_discount", + "discount", + "fulfillment", + "tax", + "fee", + "total" + ] + } + } + }, + "required": [ + "type" + ] + }, + "then": { + "required": [ + "display_text" + ] + } + } + ] + }, + "title": "TotalsCreateRequest", + "description": "Request payload to create a new Totals. Pricing breakdown provided by the business. MUST contain exactly one subtotal and one total entry. Detail types (tax, fee, discount, fulfillment) may appear multiple times for itemization. Platforms MUST render all entries in order using display_text and amount.", + "type": "array" + }, + "TotalsUpdateRequest": { + "allOf": [ + { + "contains": { + "properties": { + "type": { + "const": "subtotal", + "default": "subtotal", + "type": "string" + } + }, + "required": [ + "type" + ] + }, + "minContains": 1, + "maxContains": 1 + }, + { + "contains": { + "properties": { + "type": { + "const": "total", + "default": "total", + "type": "string" + } + }, + "required": [ + "type" + ] + }, + "minContains": 1, + "maxContains": 1 + } + ], + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/TotalUpdateRequest" + }, + { + "type": "object", + "properties": {} + }, + { + "if": { + "properties": { + "type": { + "not": { + "enum": [ + "subtotal", + "items_discount", + "discount", + "fulfillment", + "tax", + "fee", + "total" + ] + } + } + }, + "required": [ + "type" + ] + }, + "then": { + "required": [ + "display_text" + ] + } + } + ] + }, + "title": "TotalsUpdateRequest", + "description": "Request payload to update an existing Totals. Pricing breakdown provided by the business. MUST contain exactly one subtotal and one total entry. Detail types (tax, fee, discount, fulfillment) may appear multiple times for itemization. Platforms MUST render all entries in order using display_text and amount.", + "type": "array" + }, + "Ucp": { + "required": [ + "version" + ], + "type": "object", + "title": "UCP Metadata", + "description": "Protocol metadata for discovery profiles and responses. Uses slim schema pattern with context-specific required fields.", + "properties": { + "version": { + "$ref": "#/components/schemas/Version" + }, + "map_order": { + "$ref": "#/components/schemas/MapOrder", + "description": "Preferred key-traversal order for sibling registry fields inside the root `ucp` envelope (`services`, `capabilities`, and `payment_handlers`)." + }, + "status": { + "type": "string", + "enum": [ + "success", + "error" + ], + "default": "success", + "description": "Application-level status of the UCP operation." + }, + "services": { + "type": "object", + "description": "Service registry keyed by reverse-domain name.", + "propertyNames": { + "$ref": "#/components/schemas/ReverseDomainName" + }, + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ServiceBase" + } + } + }, + "capabilities": { + "type": "object", + "description": "Capability registry keyed by reverse-domain name.", + "propertyNames": { + "$ref": "#/components/schemas/ReverseDomainName" + }, + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CapabilityBase" + } + } + }, + "payment_handlers": { + "type": "object", + "description": "Payment handler registry keyed by reverse-domain name.", + "propertyNames": { + "$ref": "#/components/schemas/ReverseDomainName" + }, + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentHandlerBase" + } + } + } + } + }, + "UcpBase": { + "description": "Base UCP metadata with shared properties for all schema types.", + "type": "object", + "required": [ + "version" + ], + "properties": { + "version": { + "$ref": "#/components/schemas/Version" + }, + "map_order": { + "$ref": "#/components/schemas/MapOrder", + "description": "Preferred key-traversal order for sibling registry fields inside the root `ucp` envelope (`services`, `capabilities`, and `payment_handlers`)." + }, + "status": { + "type": "string", + "enum": [ + "success", + "error" + ], + "default": "success", + "description": "Application-level status of the UCP operation." + }, + "services": { + "type": "object", + "description": "Service registry keyed by reverse-domain name.", + "propertyNames": { + "$ref": "#/components/schemas/ReverseDomainName" + }, + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ServiceBase" + } + } + }, + "capabilities": { + "type": "object", + "description": "Capability registry keyed by reverse-domain name.", + "propertyNames": { + "$ref": "#/components/schemas/ReverseDomainName" + }, + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CapabilityBase" + } + } + }, + "payment_handlers": { + "type": "object", + "description": "Payment handler registry keyed by reverse-domain name.", + "propertyNames": { + "$ref": "#/components/schemas/ReverseDomainName" + }, + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentHandlerBase" + } + } + } + } + }, + "UcpBusinessSchema": { + "title": "UCP Business Schema", + "description": "UCP metadata for business/merchant-level configuration. Subset of platform schema with business-specific settings.", + "allOf": [ + { + "$ref": "#/components/schemas/UcpBase" + }, + { + "required": [ + "services", + "payment_handlers" + ], + "properties": { + "supported_versions": { + "type": "object", + "description": "Previous protocol versions this business supports, mapped to profile URIs. Businesses that support older protocol versions SHOULD advertise each version and link to its profile. Each URI points to a complete, self-contained profile for that version. When omitted, only `version` is supported.", + "propertyNames": { + "$ref": "#/components/schemas/Version" + }, + "additionalProperties": { + "type": "string", + "format": "uri" + } + }, + "services": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/ServiceBusinessSchema" + } + } + }, + "capabilities": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/CapabilityBusinessSchema" + } + } + }, + "payment_handlers": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/PaymentHandlerBusinessSchema" + } + } + } + } + } + ] + }, + "UcpCreateRequest": { + "required": [ + "version" + ], + "type": "object", + "title": "UcpCreateRequest", + "description": "Request payload to create a new Ucp. Protocol metadata for discovery profiles and responses. Uses slim schema pattern with context-specific required fields.", + "properties": { + "version": { + "$ref": "#/components/schemas/Version" + }, + "status": { + "type": "string", + "enum": [ + "success", + "error" + ], + "default": "success", + "description": "Application-level status of the UCP operation." + }, + "services": { + "type": "object", + "description": "Service registry keyed by reverse-domain name.", + "propertyNames": { + "$ref": "#/components/schemas/ReverseDomainName" + }, + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ServiceBase" + } + } + }, + "capabilities": { + "type": "object", + "description": "Capability registry keyed by reverse-domain name.", + "propertyNames": { + "$ref": "#/components/schemas/ReverseDomainName" + }, + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CapabilityBase" + } + } + }, + "payment_handlers": { + "type": "object", + "description": "Payment handler registry keyed by reverse-domain name.", + "propertyNames": { + "$ref": "#/components/schemas/ReverseDomainName" + }, + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentHandlerBase" + } + } + } + } + }, + "UcpEntity": { + "type": "object", + "description": "Shared foundation for all UCP entities.", + "required": [ + "version" + ], + "properties": { + "version": { + "$ref": "#/components/schemas/Version", + "description": "Entity version in YYYY-MM-DD format." + }, + "spec": { + "type": "string", + "format": "uri", + "description": "URL to human-readable specification document." + }, + "schema": { + "type": "string", + "format": "uri", + "description": "URL to JSON Schema defining this entity's structure and payloads." + }, + "id": { + "type": "string", + "description": "Unique identifier for this entity instance. Used to disambiguate when multiple instances exist." + }, + "config": { + "type": "object", + "description": "Entity-specific configuration. Structure defined by each entity's schema.", + "additionalProperties": true + } + } + }, + "UcpPlatformSchema": { + "title": "UCP Platform Schema", + "description": "Full UCP metadata for platform-level configuration. Hosted at a URI advertised by the platform in request headers.", + "allOf": [ + { + "$ref": "#/components/schemas/UcpBase" + }, + { + "required": [ + "services", + "payment_handlers" + ], + "properties": { + "services": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/ServicePlatformSchema" + } + } + }, + "capabilities": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/CapabilityPlatformSchema" + } + } + }, + "payment_handlers": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/PaymentHandlerPlatformSchema" + } + } + } + } + } + ] + }, + "UcpUpdateRequest": { + "required": [ + "version" + ], + "type": "object", + "title": "UcpUpdateRequest", + "description": "Request payload to update an existing Ucp. Protocol metadata for discovery profiles and responses. Uses slim schema pattern with context-specific required fields.", + "properties": { + "version": { + "$ref": "#/components/schemas/Version" + }, + "status": { + "type": "string", + "enum": [ + "success", + "error" + ], + "default": "success", + "description": "Application-level status of the UCP operation." + }, + "services": { + "type": "object", + "description": "Service registry keyed by reverse-domain name.", + "propertyNames": { + "$ref": "#/components/schemas/ReverseDomainName" + }, + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ServiceBase" + } + } + }, + "capabilities": { + "type": "object", + "description": "Capability registry keyed by reverse-domain name.", + "propertyNames": { + "$ref": "#/components/schemas/ReverseDomainName" + }, + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CapabilityBase" + } + } + }, + "payment_handlers": { + "type": "object", + "description": "Payment handler registry keyed by reverse-domain name.", + "propertyNames": { + "$ref": "#/components/schemas/ReverseDomainName" + }, + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentHandlerBase" + } + } + } + } + }, + "Unit": { + "allOf": [ + { + "if": { + "properties": { + "unit": { + "const": "C62", + "default": "C62", + "type": "string" + } + }, + "required": [ + "unit" + ] + }, + "then": { + "properties": { + "scale": { + "const": 0, + "default": 0, + "type": "number" + } + } + } + } + ], + "properties": { + "unit": { + "type": "string", + "description": "Stable machine identifier. The Business SHOULD use the exact UN/CEFACT Rec20 Common Code when one accurately identifies the unit. Otherwise, the Business MAY use a custom unit identifier and MUST use it consistently for the same unit. The Platform MUST treat an unrecognized identifier as opaque." + }, + "scale": { + "type": "integer", + "minimum": 0, + "maximum": 15, + "default": 0, + "description": "One step equals `10^-scale` of `unit`. When `unit` is `C62`, `scale`, if present, MUST be 0. The maximum of 15 is derived from the interoperable integer range: at scale 16 a single whole unit (10^16 steps) is no longer representable, so larger scales cannot denominate one unit of their own basis. Businesses needing finer granularity use a smaller unit." + }, + "display_text": { + "type": "string", + "description": "Required printable unit label provided by the Business. The Platform MUST use it when it does not recognize `unit`; for a recognized UN/CEFACT Rec 20 Common Code, the Platform MAY substitute its own localized label. It does not participate in unit identity or mismatch comparison." + } + }, + "title": "Unit", + "description": "A reusable unit descriptor for quantities and measures. Its unit-descriptor machine identity is (`unit`, effective `scale`), where effective `scale` is the provided `scale` or 0; `display_text` is excluded.", + "type": "object", + "required": [ + "unit", + "display_text" + ] + }, + "UnitPrice": { + "properties": { + "amount": { + "$ref": "#/components/schemas/Amount", + "description": "Unit price in ISO 4217 minor units. After satisfying the same-unit invariant, the Business MUST compute the comparator as `(price.amount / (measure.value × 10^-measure.scale)) × (reference.value × 10^-reference.scale)` and round it once to ISO 4217 minor units according to its pricing rules. The returned `unit_price.amount` is authoritative; the Platform MUST NOT recompute or substitute its own result." + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$", + "description": "ISO 4217 currency code." + }, + "measure": { + "description": "Product quantity in packaging/content (for example, a 750 mL bottle), distinct from `quantity_unit`, which defines the sale basis. Its integer `value` MUST be at least 1.", + "allOf": [ + { + "$ref": "#/components/schemas/Measure" + }, + { + "type": "object", + "properties": { + "value": { + "minimum": 1 + } + } + } + ] + }, + "reference": { + "description": "Denominator for unit price display (for example, per 100 mL or per 1 kg). Its integer `value` MUST be at least 1.", + "allOf": [ + { + "$ref": "#/components/schemas/Measure" + }, + { + "type": "object", + "properties": { + "value": { + "minimum": 1 + } + } + } + ] + } + }, + "required": [ + "amount", + "currency", + "measure", + "reference" + ], + "title": "Unit Price", + "description": "Price per standard unit of measurement. MAY be omitted when unit pricing does not apply. `unit_price.currency` MUST equal `price.currency`; the comparator MUST NOT perform currency conversion. `measure.unit` and `reference.unit` MUST be identical; cross-unit conversion is not permitted. Their scales MAY differ; each value represents `value × 10^-scale`.", + "type": "object" + }, + "ValueConstraint": { + "type": "object", + "description": "A Value Constraint containing `enum`, `const`, or both.", + "anyOf": [ + { + "required": [ + "enum" + ], + "type": "object", + "properties": { + "enum": { + "type": "array", + "description": "A non-empty array of unique JSON values.", + "minItems": 1, + "uniqueItems": true + }, + "const": {}, + "default": {} + }, + "additionalProperties": false + }, + { + "required": [ + "const" + ], + "type": "object", + "properties": { + "enum": { + "type": "array", + "description": "A non-empty array of unique JSON values.", + "minItems": 1, + "uniqueItems": true + }, + "const": {}, + "default": {} + }, + "additionalProperties": false + } + ], + "properties": { + "enum": { + "type": "array", + "description": "A non-empty array of unique JSON values.", + "minItems": 1, + "uniqueItems": true + }, + "const": {}, + "default": {} + }, + "additionalProperties": false + }, + "Variant": { + "properties": { + "id": { + "type": "string", + "description": "Global ID (GID) uniquely identifying this variant. Used as item.id in checkout." + }, + "sku": { + "type": "string", + "description": "Business-assigned identifier for inventory and fulfillment." + }, + "barcodes": { + "type": "array", + "items": { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "description": "Barcode standard. Well-known values: UPC, EAN, ISBN, GTIN, JAN." + }, + "value": { + "type": "string", + "description": "Barcode value." + } + } + }, + "description": "Industry-standard product identifiers for cross-reference and correlation." + }, + "handle": { + "type": "string", + "description": "URL-safe variant handle/slug." + }, + "title": { + "type": "string", + "description": "Variant display title (e.g., 'Blue / Large')." + }, + "description": { + "$ref": "#/components/schemas/Description", + "description": "Variant description in one or more formats." + }, + "url": { + "type": "string", + "format": "uri", + "description": "Canonical variant page URL." + }, + "categories": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Category" + }, + "description": "Variant categories with optional taxonomy identifiers." + }, + "price": { + "$ref": "#/components/schemas/Price", + "description": "Current selling price. Price is the amount per one whole `quantity_unit.unit` (for example, per lb or per hour); when `quantity_unit` is absent, it is per `each`. Line total is `price × quantity × 10^-scale`, computed and rounded once by the Business; `totals` remain authoritative." + }, + "quantity_unit": { + "$ref": "#/components/schemas/QuantityUnit", + "description": "Sale basis this variant's `quantity` is denominated in. The default sale basis is `each`, whose machine identity is (`C62`, 0); `C62` is the UN/CEFACT Rec20 code for one/each. An absent catalog descriptor encodes that default. An `increment` advertises the ordering granularity in steps (for example, `scale` 2 with `increment` 25 sells in 0.25-unit multiples)." + }, + "list_price": { + "$ref": "#/components/schemas/Price", + "description": "List price before discounts (for strikethrough display)." + }, + "unit_price": { + "$ref": "#/components/schemas/UnitPrice", + "description": "Price per standard unit of measurement, for shelf-style comparison display. MAY be omitted when unit pricing does not apply." + }, + "availability": { + "$ref": "#/components/schemas/Availability", + "description": "Variant availability for purchase." + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SelectedOption" + }, + "description": "Option values that define this variant (e.g., Color: Blue, Size: Large)." + }, + "media": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Media" + }, + "description": "Variant media (images, videos, 3D models). First item is the featured media for listings." + }, + "rating": { + "$ref": "#/components/schemas/Rating", + "description": "Variant rating." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Variant tags for categorization and search." + }, + "metadata": { + "type": "object", + "description": "Business-defined custom data extending the standard variant model." + }, + "seller": { + "type": "object", + "description": "Optional seller context for this variant.", + "properties": { + "name": { + "type": "string", + "description": "Seller display name." + }, + "links": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Link" + }, + "description": "Seller policy and information links." + } + } + } + }, + "required": [ + "id", + "title", + "description", + "price" + ], + "title": "Variant", + "description": "A purchasable variant of a product with specific option selections.", + "type": "object" + }, + "Version": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$", + "description": "Version identifier in YYYY-MM-DD format." + }, + "VersionConstraint": { + "type": "object", + "description": "Version range requirement with minimum and optional maximum.", + "properties": { + "min": { + "$ref": "#/components/schemas/Version", + "description": "Minimum required version (inclusive)." + }, + "max": { + "$ref": "#/components/schemas/Version", + "description": "Maximum compatible version (inclusive). When absent, no upper bound." + } + }, + "required": [ + "min" + ], + "additionalProperties": true + }, + "WarningCode": { + "examples": [ + "final_sale", + "prop65", + "fulfillment_changed", + "payment_term_changed", + "age_restricted" + ], + "type": "string", + "title": "Warning Code", + "description": "Warning code identifying the type of warning. Standard codes are defined in capability specifications (see examples) and have standardized semantics; freeform codes are permitted." + } + }, + "parameters": { + "IdempotencyKey": { + "name": "Idempotency-Key", + "in": "header", + "description": "UUID v4 idempotency token to safely retry mutating operations without side-effects.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + "Signature": { + "name": "Signature", + "in": "header", + "description": "RFC 9421 HTTP Message Signature value.", + "required": false, + "schema": { + "type": "string" + } + }, + "SignatureAgent": { + "name": "Signature-Agent", + "in": "header", + "description": "Web Bot Auth key lookup metadata (type=jwks_uri, type=cimd, or type=directory).", + "required": false, + "schema": { + "type": "string" + } + }, + "SignatureInput": { + "name": "Signature-Input", + "in": "header", + "description": "RFC 9421 Signature input metadata describing signed components.", + "required": false, + "schema": { + "type": "string" + } + }, + "UcpAgent": { + "name": "UCP-Agent", + "in": "header", + "description": "Client or platform identification string (e.g., platform/1.0.0; agent/2.1.0).", + "required": true, + "schema": { + "type": "string" + } + } + }, + "securitySchemes": { + "BearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + "description": "Bearer token authentication for authenticated buyer sessions." + }, + "HttpSignatureAuth": { + "type": "http", + "scheme": "signature", + "description": "RFC 9421 HTTP Message Signatures permissionless authentication." + } + } + }, + "security": [ + { + "HttpSignatureAuth": [] + }, + { + "BearerAuth": [] + } + ], + "tags": [ + { + "name": "Cart", + "description": "Shopping cart with estimated pricing before checkout. Lightweight pre-purchase exploration with no payment info or complex status states." + }, + { + "name": "Checkout", + "description": "Base checkout schema. Extensions compose onto this using allOf." + }, + { + "name": "Order", + "description": "Order schema with line items, buyer-facing fulfillment expectations, and event logs." + }, + { + "name": "Discovery", + "description": "Merchant profile and capability discovery." + }, + { + "name": "Catalog", + "description": "Product/variant lookup by identifier. Supports batch retrieval (lookup_catalog) and single-product detail (get_product)." + } + ] +} \ No newline at end of file diff --git a/src/ucp_sdk/models/__init__.py b/src/ucp_sdk/models/__init__.py index bb24223..31c82a0 100644 --- a/src/ucp_sdk/models/__init__.py +++ b/src/ucp_sdk/models/__init__.py @@ -13,3 +13,24 @@ # limitations under the License. """UCP models.""" + +from .schemas import models +from .schemas.models import * # noqa: F403 + +# Compatibility aliases for unannotated request types & models +PaymentCreateRequest = models.Payment +PaymentUpdateRequest = models.Payment +PaymentCompleteRequest = models.Payment +OrderUpdateRequest = models.Order +AttributionCreateRequest = models.Attribution +AttributionUpdateRequest = models.Attribution +AttributionCompleteRequest = models.Attribution +BuyerCreateRequest = models.Buyer +BuyerUpdateRequest = models.Buyer +LocalityCreateRequest = models.Locality +LocalityUpdateRequest = models.Locality +ContextCreateRequest = models.Context +ContextUpdateRequest = models.Context +LineItemModel = models.LineItem + +__all__ = ["models"] diff --git a/src/ucp_sdk/models/schemas/__init__.py b/src/ucp_sdk/models/schemas/__init__.py index 1252d6b..13fa6b8 100644 --- a/src/ucp_sdk/models/schemas/__init__.py +++ b/src/ucp_sdk/models/schemas/__init__.py @@ -12,6 +12,25 @@ # See the License for the specific language governing permissions and # limitations under the License. -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""UCP schema models.""" + +from .models import * # noqa: F403 +from . import models + +# Compatibility aliases for unannotated request types & models +PaymentCreateRequest = models.Payment +PaymentUpdateRequest = models.Payment +PaymentCompleteRequest = models.Payment +OrderUpdateRequest = models.Order +AttributionCreateRequest = models.Attribution +AttributionUpdateRequest = models.Attribution +AttributionCompleteRequest = models.Attribution +BuyerCreateRequest = models.Buyer +BuyerUpdateRequest = models.Buyer +LocalityCreateRequest = models.Locality +LocalityUpdateRequest = models.Locality +ContextCreateRequest = models.Context +ContextUpdateRequest = models.Context +LineItemModel = models.LineItem + +__all__ = ["models"] diff --git a/src/ucp_sdk/models/schemas/models.py b/src/ucp_sdk/models/schemas/models.py new file mode 100644 index 0000000..70e89b5 --- /dev/null +++ b/src/ucp_sdk/models/schemas/models.py @@ -0,0 +1,3636 @@ +# Copyright 2026 UCP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# generated by datamodel-codegen +# pylint: disable=all +# pyformat: disable + +from __future__ import annotations + +from typing import Annotated, Any, Literal + +from pydantic import AnyUrl, AwareDatetime, BaseModel, ConfigDict, Field +from typing_extensions import TypeAliasType + +Amount = TypeAliasType( + "Amount", + Annotated[int, Field(..., ge=0, le=9007199254740991, title="Amount")], +) +""" +Monetary amount in the currency's minor unit as defined by ISO 4217. Refer to the currency's exponent to determine minor-to-major ratio (e.g., 2 for USD, 0 for JPY, 3 for KWD). +""" + + +Attribution = TypeAliasType("Attribution", dict[str, str]) +""" +Platform-emitted referral and conversion-event context — campaign identifiers, click IDs, source/medium markers, etc. The same parameters platforms communicate via URL query parameters in browser-based flows. +""" + + +class Availability(BaseModel): + """ + Availability of an item: whether it can be obtained, and a qualifying status. + """ + + available: bool | None = None + """ + Whether this can be obtained. See status for fulfillment details. + """ + status: str | None = None + """ + Qualifies available with fulfillment state. Well-known values: `in_stock`, `backorder`, `preorder`, `out_of_stock`, `discontinued`. + """ + + +class MultiDestinationItem(BaseModel): + model_config = ConfigDict( + extra="allow", + ) + method: str + """ + Fulfillment method type (e.g. `shipping`, `pickup`). Optional per-method constraints MAY be added alongside. + """ + + +class BusinessFulfillmentConfig(BaseModel): + """ + Business's fulfillment configuration. + """ + + multi_destination: list[MultiDestinationItem] | None = None + """ + Method types that permit multiple destinations within one cart (e.g. split shipping across addresses). Listing a method permits it; an omitted method does not. Open — businesses MAY list any method type. + """ + method_combinations: list[list[str]] | None = None + """ + Method-type combinations the business permits within one cart. Each inner array is a permitted set of method `type` values (e.g. shipping + pickup). + """ + + +class Buyer(BaseModel): + model_config = ConfigDict( + extra="allow", + ) + first_name: str | None = None + """ + First name of the buyer. + """ + last_name: str | None = None + """ + Last name of the buyer. + """ + email: str | None = None + """ + Email of the buyer. + """ + phone_number: str | None = None + """ + E.164 standard. + """ + + +BuyerConsent = TypeAliasType( + "BuyerConsent", Annotated[Any, Field(..., title="Buyer Consent Extension")] +) +""" +Extends the buyer object with per-purpose consent. Each purpose is keyed by a reverse-DNS identifier and carries the current `granted` state, the `source` of that state (business default or platform-captured buyer decision), a `description`, optional `links`, and optional `segments` for finer-grained channel, vendor, or program decisions scoped to that purpose. +""" + + +class Category(BaseModel): + """ + A product category with optional taxonomy identifier. + """ + + value: str + """ + Category value or path (e.g., 'Apparel > Shirts', '1604'). + """ + taxonomy: str | None = None + """ + Source taxonomy. Well-known values: `google_product_category`, `shopify`, `merchant`. + """ + + +class Description(BaseModel): + """ + Description content in one or more formats. At least one format must be provided. + """ + + plain: str | None = None + """ + Plain text content. + """ + html: str | None = None + """ + HTML-formatted content. Security: Platforms MUST sanitize before rendering—strip scripts, event handlers, and untrusted elements. Treat all rich text as untrusted input. + """ + markdown: str | None = None + """ + Markdown-formatted content. + """ + + +Discount = TypeAliasType( + "Discount", Annotated[Any, Field(..., title="Discount Extension")] +) +""" +Extends Cart and Checkout with discount support, including discount codes, automatic discounts, and eligibility-triggered provisional discounts. +""" + + +class EmbeddedConfig(BaseModel): + """ + Per-session configuration for embedded transport binding. Allows businesses to vary EP availability and delegations based on cart contents, agent authorization, or policy. + """ + + delegate: list[str] | None = None + """ + Delegations the business allows. At service-level, declares available delegations. In UCP responses, confirms accepted delegations for this session. + """ + color_scheme: list[Literal["light", "dark"]] | None = None + """ + Color schemes the business supports. Hosts use ec_color_scheme query parameter to request a scheme from this list. + """ + + +Endpoint = TypeAliasType("Endpoint", AnyUrl) +""" +Absolute HTTPS browser endpoint with a non-empty authority and without userinfo, query, fragment, whitespace, backslashes, or trailing slash. Optional compact item path and query parameters are appended to this endpoint. +""" + + +ErrorCode = TypeAliasType( + "ErrorCode", + Annotated[ + str, + Field( + ..., + examples=[ + "not_found", + "out_of_stock", + "item_unavailable", + "address_undeliverable", + "payment_failed", + "eligibility_invalid", + "identity_required", + "insufficient_scope", + ], + title="Error Code", + ), + ], +) +""" +Error code identifying the type of error. Standard errors are defined in capability specifications (see examples) and have standardized semantics; freeform codes are permitted. +""" + + +class LineItem1(BaseModel): + id: str + """ + Line item ID reference. + """ + quantity: int = Field(..., ge=1, le=9007199254740991) + """ + Integer count of steps of the referenced line item's `quantity_unit` (`10^-scale` × `unit`); when `quantity_unit` is absent, it counts whole items (`each`). + """ + + +class FulfillmentAvailableMethod(BaseModel): + """ + Inventory availability hint for a fulfillment method type. + """ + + model_config = ConfigDict( + extra="allow", + ) + type: str + """ + Fulfillment method type this availability applies to. Well-known values: `shipping`, `pickup`; businesses MAY use additional values. + """ + line_item_ids: list[str] + """ + Line items available for this fulfillment method. + """ + fulfillable_on: str | None = None + """ + 'now' for immediate availability, or ISO 8601 date for future (preorders, transfers). + """ + description: str | None = None + """ + Human-readable availability info (e.g., 'Available for pickup at Downtown Store today'). + """ + + +class FulfillmentEvent(BaseModel): + """ + Append-only fulfillment event representing an actual shipment. References line items by ID. + """ + + id: str + """ + Fulfillment event identifier. + """ + occurred_at: AwareDatetime + """ + RFC 3339 timestamp when this fulfillment event occurred. + """ + type: str + """ + Fulfillment event type. Common values include: processing (preparing to ship), shipped (handed to carrier), in_transit (in delivery network), delivered (received by buyer), failed_attempt (delivery attempt failed), canceled (fulfillment canceled), undeliverable (cannot be delivered), returned_to_sender (returned to merchant). + """ + line_items: list[LineItem1] + """ + Which line items and quantities are fulfilled in this event. + """ + tracking_number: str | None = None + """ + Carrier tracking number (required if type != processing). + """ + tracking_url: AnyUrl | None = None + """ + URL to track this shipment (required if type != processing). + """ + carrier: str | None = None + """ + Carrier name (e.g., 'FedEx', 'USPS'). + """ + description: str | None = None + """ + Human-readable description of the shipment status or delivery information (e.g., 'Delivered to front door', 'Out for delivery'). + """ + + +class FulfillmentGroupCreateRequest(BaseModel): + """ + Request payload to create a new FulfillmentGroup. A merchant-generated package/group of line items with fulfillment options. + """ + + model_config = ConfigDict( + extra="allow", + ) + selected_option_id: str | None = None + """ + ID of the selected fulfillment option for this group. + """ + + +class FulfillmentGroupUpdateRequest(BaseModel): + """ + Request payload to update an existing FulfillmentGroup. A merchant-generated package/group of line items with fulfillment options. + """ + + model_config = ConfigDict( + extra="allow", + ) + id: str + """ + Group identifier for referencing merchant-generated groups in updates. + """ + selected_option_id: str | None = None + """ + ID of the selected fulfillment option for this group. + """ + + +class FulfillmentMethodCreateRequest(BaseModel): + """ + Request payload to create a new FulfillmentMethod. A fulfillment method with destinations and groups. + """ + + type: str + """ + Fulfillment method type. Well-known values: `shipping`, `pickup`. Businesses MAY use additional values. + """ + selected_destination_id: str | None = None + """ + ID of the selected destination. Accepts any stable, Business-scoped ID the Business recognizes for this method, including Location IDs not yet enumerated in `destinations`. + """ + groups: list[FulfillmentGroupCreateRequest] | None = None + """ + Fulfillment groups for selecting options. Agent sets selected_option_id on groups to choose shipping method. + """ + + +class FulfillmentMethodUpdateRequest(BaseModel): + """ + Request payload to update an existing FulfillmentMethod. A fulfillment method with destinations and groups. + """ + + id: str | None = None + """ + Unique fulfillment method identifier. + """ + type: str | None = None + """ + Fulfillment method type. Well-known values: `shipping`, `pickup`. Businesses MAY use additional values. + """ + line_item_ids: list[str] + """ + Line item IDs fulfilled via this method. + """ + selected_destination_id: str | None = None + """ + ID of the selected destination. Accepts any stable, Business-scoped ID the Business recognizes for this method, including Location IDs not yet enumerated in `destinations`. + """ + groups: list[FulfillmentGroupUpdateRequest] | None = None + """ + Fulfillment groups for selecting options. Agent sets selected_option_id on groups to choose shipping method. + """ + + +class FulfillmentOptionBase(BaseModel): + """ + Common base for a fulfillment option: an addressable, renderable choice (e.g. Standard, Express). Catalog uses this base directly; checkout composes it with cost and timing. + """ + + model_config = ConfigDict( + extra="allow", + ) + id: str + """ + Unique identifier for this fulfillment option. + """ + title: str + """ + Short label that distinguishes this option from its siblings (e.g. 'Standard', 'Express Shipping', 'Curbside Pickup'). + """ + description: Description | None = None + """ + Supplementary context for the title (e.g. 'Arrives in 4 business days', 'Arrives Dec 12-15 via FedEx'). Directly renderable; MUST NOT repeat the title. + """ + + +class FulfillmentOptionCreateRequest(FulfillmentOptionBase): + """ + Request payload to create a new FulfillmentOption. A fulfillment option within a group (e.g., Standard Shipping $5, Express $15). Extends the fulfillment option base with cost and timing. + """ + + +class FulfillmentOptionUpdateRequest(FulfillmentOptionBase): + """ + Request payload to update an existing FulfillmentOption. A fulfillment option within a group (e.g., Standard Shipping $5, Express $15). Extends the fulfillment option base with cost and timing. + """ + + +class FulfillmentUpdateRequest(BaseModel): + """ + Request payload to update an existing Fulfillment. Container for fulfillment methods and availability. + """ + + methods: list[FulfillmentMethodUpdateRequest] | None = None + """ + Fulfillment methods for cart items. + """ + + +InfoCode = TypeAliasType( + "InfoCode", + Annotated[ + str, + Field( + ..., + examples=[ + "identity_optional", + "signal", + "free_shipping", + "not_found", + ], + title="Info Code", + ), + ], +) +""" +Info code identifying the type of informational message. Standard codes are defined in capability specifications (see examples) and have standardized semantics; freeform codes are permitted. +""" + + +class InputCorrelation(BaseModel): + """ + Maps a request identifier to the variant it resolved to, with match semantics. + """ + + id: str + """ + The identifier from the lookup request that resolved to this variant. + """ + match: str | None = Field(None, examples=["exact", "featured"]) + """ + How the request identifier resolved to this variant. Well-known values: `exact` (input directly identifies this variant, e.g., variant ID, SKU), `featured` (server selected this variant as representative, e.g., product ID resolved to best match). Businesses MAY implement and provide additional resolution strategies. + """ + + +class Instance(BaseModel): + """ + Common fields for one outstanding Action instance are id and optional config. The extension declaring the Action type defines type-specific processing data under config. Additional properties are permitted for forward compatibility. + """ + + model_config = ConfigDict( + extra="allow", + ) + id: str = Field(..., min_length=1) + """ + Identifier for this Action instance. + """ + config: dict[str, Any] | None = None + """ + Configuration defined by the extension that declares this Action type. + """ + + +class JwkPublicKey(BaseModel): + """ + Public JSON Web Key used for HTTP Message Signatures and signed webhook verification. UCP profiles publish public keys only; private key material MUST NOT appear in a profile. Well-known key types: EC (ECDSA P-256, P-384) and OKP (EdDSA Ed25519); OKP keys are RECOMMENDED for signers opting into Web Bot Auth (WBA) interop on HTTP transport. A single profile MAY publish keys of either or both types; consumers select keys by kid. The kty, crv, and alg vocabularies are OPEN: verifiers MUST tolerate key types, curves, and algorithms they do not recognize, selecting keys by kid at verification time. An unsupported key affects only the signature that references it (algorithm_unsupported) and MUST NOT cause whole-profile rejection. Additional public JWK members are permitted; consumers ignore unknown members. + """ + + model_config = ConfigDict( + extra="allow", + ) + kid: str + """ + Key identifier referenced by Signature-Input keyid. For keys used in dual-audience (Web Bot Auth) signatures, the kid MUST be the key's JWK SHA-256 Thumbprint (RFC 7638) so UCP-Agent and Signature-Agent lookups resolve the same key; otherwise the kid MAY be any stable string. + """ + kty: str = Field(..., examples=["EC", "OKP"]) + """ + JWK key type. Well-known values: EC for ECDSA (P-256, P-384); OKP for EdDSA (Ed25519). Open vocabulary; verifiers tolerate unrecognized types and select keys by kid. + """ + crv: str | None = Field(None, examples=["P-256", "P-384", "Ed25519"]) + """ + Curve name. Well-known values: P-256, P-384 (EC); Ed25519 (OKP). Open vocabulary. + """ + x: str | None = None + """ + Public key value, base64url-encoded. For EC, the x coordinate (RFC 7518 §6.2); for OKP, the public key (RFC 8037 §2). + """ + y: str | None = None + """ + EC public key y coordinate, base64url-encoded (RFC 7518 §6.2). Not used by OKP keys. + """ + alg: str | None = Field(None, examples=["ES256", "ES384", "EdDSA"]) + """ + JWA algorithm associated with this public key. Optional; verifiers derive the algorithm from crv when alg is omitted. When present for a well-known curve it MUST match: ES256 with P-256, ES384 with P-384, EdDSA with Ed25519. + """ + use: str | None = None + """ + JWK public key use. UCP examples use sig for signatures. + """ + + +class Link(BaseModel): + type: str + """ + Type of link. Well-known values: `privacy_policy`, `terms_of_service`, `refund_policy`, `shipping_policy`, `faq`. Consumers SHOULD handle unknown values gracefully by displaying them using the `title` field or omitting the link. + """ + url: AnyUrl + """ + The actual URL pointing to the content to be displayed. + """ + title: str | None = None + """ + Optional display text for the link. When provided, use this instead of generating from type. + """ + + +class Locality(BaseModel): + """ + A coarse geographic location — country, region, and postal code. A lightweight alternative to a full postal address. + """ + + model_config = ConfigDict( + extra="allow", + ) + address_country: str | None = None + """ + The country, as a 2-letter ISO 3166-1 alpha-2 code (e.g. "US"). A 3-letter alpha-3 code or full country name MAY also be used. + """ + address_region: str | None = None + """ + The first-level administrative region within the country (e.g. a state or province such as California). + """ + postal_code: str | None = None + """ + The postal code (e.g. "94043"). + """ + + +class LocationSummaryCreateRequest(BaseModel): + """ + Request payload to create a new LocationSummary. A summary of a physical business location. + """ + + model_config = ConfigDict( + extra="allow", + ) + id: str + """ + Stable, opaque, Business-scoped Location identifier. + """ + + +class LocationSummaryUpdateRequest(BaseModel): + """ + Request payload to update an existing LocationSummary. A summary of a physical business location. + """ + + model_config = ConfigDict( + extra="allow", + ) + id: str + """ + Stable, opaque, Business-scoped Location identifier. + """ + + +MapOrder = TypeAliasType("MapOrder", dict[str, list[str]]) +""" +Preferred key order for map-valued fields in the scope annotated by the containing `ucp` member. Each property names a target map, and its array lists target keys in preferred order. Lists may be partial and are not allowlists. +""" + + +class Media(BaseModel): + """ + Media item (image, video, etc.). + """ + + type: str + """ + Media type. Well-known values: `image`, `video`, `model_3d`. + """ + url: AnyUrl + """ + URL to the media resource. + """ + alt_text: str | None = None + """ + Accessibility text describing the media. + """ + width: int | None = Field(None, ge=1) + """ + Width in pixels (for images/video). + """ + height: int | None = Field(None, ge=1) + """ + Height in pixels (for images/video). + """ + + +class MessageError(BaseModel): + type: Literal["error"] = "error" + """ + Message type discriminator. + """ + code: ErrorCode + path: str | None = None + """ + RFC 9535 JSONPath to the component the message refers to (e.g., $.line_items[0]). + """ + content_type: Literal["plain", "markdown"] | None = "plain" + """ + Content format, default = plain. + """ + content: str + """ + Human-readable message. + """ + severity: Literal[ + "recoverable", + "requires_buyer_input", + "requires_buyer_review", + "unrecoverable", + ] + """ + Reflects the resource state and recommended action. 'recoverable': platform can resolve the condition in band, for example by modifying inputs or processing a related Action, and submit a new operation when needed. 'requires_buyer_input': merchant requires information their API doesn't support collecting programmatically (checkout incomplete). 'requires_buyer_review': buyer must authorize before order placement due to policy, regulatory, or entitlement rules. 'unrecoverable': no valid resource exists to act on, retry with new resource or inputs. Errors with 'requires_*' severity contribute to 'status: requires_escalation'. + """ + + +class MessageInfo(BaseModel): + type: Literal["info"] = "info" + """ + Message type discriminator. + """ + path: str | None = None + """ + RFC 9535 JSONPath to the component the message refers to (e.g., $.line_items[0]). + """ + code: InfoCode | None = None + content_type: Literal["plain", "markdown"] | None = "plain" + """ + Content format, default = plain. + """ + content: str + """ + Human-readable message. + """ + + +class OptionValue(BaseModel): + """ + A selectable value for a product option. + """ + + id: str | None = None + """ + Optional server-assigned identifier for this option value. When present in a selected_option, the server SHOULD use it for matching instead of label. + """ + label: str + """ + Display text for this option value (e.g., 'Small', 'Blue'). + """ + + +class OrderConfirmation(BaseModel): + """ + Order details available at the time of checkout completion. + """ + + id: str + """ + Unique order identifier. + """ + label: str | None = None + """ + Human-readable label for identifying the order. MUST only be provided by the business. + """ + permalink_url: AnyUrl + """ + Permalink to access the order on merchant site. + """ + + +class Quantity(BaseModel): + """ + Tracks the line item's original, current active, and fulfilled quantities. All three values use the same inherited `item.quantity_unit`. When `item.quantity_unit` is absent on an authoritative order response, each step is one whole item (`each`) under the shared default. + """ + + original: int | None = Field(None, ge=0, le=9007199254740991) + """ + Quantity from the original checkout, expressed as an integer step count. + """ + total: int = Field(..., ge=0, le=9007199254740991) + """ + Current active quantity after returns, cancellations, or other order changes, expressed as an integer step count. + """ + fulfilled: int = Field(..., ge=0, le=9007199254740991) + """ + Quantity fulfilled so far, expressed as an integer step count. + """ + + +class OrderPlatformSchema(BaseModel): + """ + Platform's order capability configuration. + """ + + webhook_url: AnyUrl + """ + URL where merchant sends order lifecycle events (webhooks). + """ + + +class PaymentCredential(BaseModel): + """ + The base definition for any payment credential. Handlers define specific credential types. + """ + + model_config = ConfigDict( + extra="allow", + ) + type: str + """ + The credential type discriminator. Specific schemas will constrain this to a constant value. + """ + + +class PlatformFulfillmentConfig(BaseModel): + """ + Platform's fulfillment configuration. + """ + + supports_multi_group: bool | None = False + """ + Enables multiple groups per method. + """ + + +class PostalAddress(BaseModel): + extended_address: str | None = None + """ + An address extension such as an apartment number, C/O or alternative name. + """ + street_address: str | None = None + """ + The street address. + """ + address_locality: str | None = None + """ + The locality in which the street address is, and which is in the region. For example, Mountain View. + """ + address_region: str | None = None + """ + The region in which the locality is, and which is in the country. Required for applicable countries (i.e. state in US, province in CA). For example, California or another appropriate first-level Administrative division. + """ + address_country: str | None = None + """ + The country. Recommended to be in 2-letter ISO 3166-1 alpha-2 format, for example "US". For backward compatibility, a 3-letter ISO 3166-1 alpha-3 country code such as "SGP" or a full country name such as "Singapore" can also be used. + """ + postal_code: str | None = None + """ + The postal code. For example, 94043. + """ + first_name: str | None = None + """ + Optional. First name of the contact associated with the address. + """ + last_name: str | None = None + """ + Optional. Last name of the contact associated with the address. + """ + phone_number: str | None = None + """ + Optional. Phone number of the contact associated with the address. + """ + + +class Price(BaseModel): + """ + Price with explicit currency. + """ + + amount: Amount + """ + Amount in ISO 4217 minor units. Use 0 for free items. + """ + currency: str = Field(..., pattern="^[A-Z]{3}$") + """ + ISO 4217 currency code (e.g., 'USD', 'EUR', 'GBP'). + """ + + +class PriceFilter(BaseModel): + """ + Price range filter denominated in context.currency. When context.currency matches the presentment currency, businesses apply the filter directly. When it differs, businesses SHOULD convert filter values to the presentment currency before applying; if conversion is not supported, businesses MAY ignore the filter and SHOULD indicate this via a message. When context.currency is absent, filter denomination is ambiguous and businesses MAY ignore it. + """ + + min: Amount | None = None + """ + Minimum price in ISO 4217 minor units. + """ + max: Amount | None = None + """ + Maximum price in ISO 4217 minor units. + """ + + +class PriceRange(BaseModel): + """ + A price range representing minimum and maximum values (e.g., a common example in retail shopping is when prices vary across product variants). + """ + + min: Price + """ + Minimum price in the range. + """ + max: Price + """ + Maximum price in the range. + """ + + +class ProductOption(BaseModel): + """ + A product option such as size, color, or material. + """ + + name: str + """ + Option name (e.g., 'Size', 'Color'). + """ + values: list[OptionValue] = Field(..., min_length=1) + """ + Available values for this option. + """ + + +class Rating(BaseModel): + """ + Product rating aggregate. + """ + + value: float = Field(..., ge=0.0) + """ + Average rating value. + """ + scale_min: float | None = Field(1, ge=0.0) + """ + Minimum value on the rating scale (e.g., 1 for 1-5 stars). + """ + scale_max: float = Field(..., ge=1.0) + """ + Maximum value on the rating scale (e.g., 5 for 5-star). + """ + count: int | None = Field(None, ge=0) + """ + Number of reviews contributing to the rating. + """ + + +class Request(BaseModel): + """ + Pagination parameters for requests. + """ + + cursor: str | None = None + """ + Opaque cursor from previous response. + """ + limit: int | None = Field(None, ge=1) + """ + Requested page size, not a guaranteed result count. When omitted, the Business MUST apply a default page size. A default of 10 is RECOMMENDED, but the Business MAY choose another value. The Business MAY return fewer results than the requested or default page size, including when enforcing its maximum page size. A Platform MUST NOT assume that the response count equals either value. + """ + + +class Response(BaseModel): + """ + Pagination information in responses. + """ + + cursor: str | None = None + """ + Cursor to fetch the next page of results. MUST be present when has_next_page is true. + """ + has_next_page: bool + """ + Whether more results are available. + """ + total_count: int | None = Field(None, ge=0) + """ + Total number of matching items, if available. + """ + + +ReverseDomainName = TypeAliasType( + "ReverseDomainName", + Annotated[ + str, + Field( + ..., + examples=[ + "dev.ucp.shopping.checkout", + "dev.ucp.common.identity_linking", + "com.example.loyalty_gold", + "com.example-shop.checkout", + "com.2example.cart", + "uk.co.example-shop.checkout", + "xn--p1ai.example.checkout", + ], + pattern="^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?)+$", + title="Reverse Domain Name", + ), + ], +) +""" +Reverse-domain identifier used for collision-safe namespacing of capabilities, services, handlers, eligibility claims, and extension-contributed keys. Must contain at least two dot-separated segments (e.g., 'dev.ucp.shopping.checkout', 'com.example.loyalty_gold'). Segments after the first are domain- or identifier-derived: they may contain interior hyphens, may start with a digit, and may contain underscores (e.g., 'com.example-shop.checkout', 'com.2example.cart', 'dev.ucp.common.identity_linking'), but must not start or end with a hyphen. The first segment (the reversed top-level domain) is letters and digits, and may contain interior hyphens to support internationalized (punycode) top-level domains such as 'xn--p1ai'. +""" + + +class SearchFilters(BaseModel): + """ + Filter criteria to narrow search results. All specified filters combine with AND logic. + """ + + model_config = ConfigDict( + extra="allow", + ) + categories: list[str] | None = None + """ + Filter by product categories (OR logic — matches products in any listed categories). Values match against the value field in product category entries. Valid values can be discovered from the categories field in search results, merchant documentation, or standard taxonomies that businesses may align with. + """ + price: PriceFilter | None = None + + +class SelectedOption(BaseModel): + """ + A specific option selection on a variant (e.g., Size: Large). + """ + + name: str + """ + Option name (e.g., 'Size'). + """ + id: str | None = None + """ + Optional option value identifier from option_value.id. When present, the server SHOULD use it for matching; name and label remain required for display. + """ + label: str + """ + Selected option label (e.g., 'Large'). + """ + + +class ServiceBusinessSchema1(BaseModel): + transport: Literal["rest"] = "rest" + + +class ServiceBusinessSchema2(BaseModel): + transport: Literal["mcp"] = "mcp" + + +class ServiceBusinessSchema3(BaseModel): + transport: Literal["a2a"] = "a2a" + + +class ServiceBusinessSchema4(BaseModel): + transport: Literal["embedded"] = "embedded" + config: EmbeddedConfig | None = None + + +class ServicePlatformSchema1(ServiceBusinessSchema1): + pass + + +class ServicePlatformSchema2(ServiceBusinessSchema2): + pass + + +class ServicePlatformSchema3(ServiceBusinessSchema3): + pass + + +class ServicePlatformSchema4(BaseModel): + transport: Literal["embedded"] = "embedded" + + +class ServiceResponseSchema1(ServiceBusinessSchema1): + pass + + +class ServiceResponseSchema2(ServiceBusinessSchema2): + pass + + +class ServiceResponseSchema3(ServiceBusinessSchema3): + pass + + +class ServiceResponseSchema4(ServiceBusinessSchema4): + pass + + +class ShippingDestination(PostalAddress): + """ + Shipping destination. + """ + + id: str + """ + ID specific to this shipping destination. + """ + type: Literal["shipping_address"] = "shipping_address" + """ + Destination type discriminator. + """ + + +class ShippingDestinationCreateRequest(PostalAddress): + """ + Request payload to create a new ShippingDestination. Shipping destination. + """ + + id: str | None = None + """ + ID specific to this shipping destination. + """ + type: Literal["shipping_address"] = "shipping_address" + """ + Destination type discriminator. + """ + + +class ShippingDestinationUpdateRequest(PostalAddress): + """ + Request payload to update an existing ShippingDestination. Shipping destination. + """ + + id: str | None = None + """ + ID specific to this shipping destination. + """ + type: Literal["shipping_address"] = "shipping_address" + """ + Destination type discriminator. + """ + + +class Signals(BaseModel): + """ + Environment data provided by the platform to support authorization and abuse prevention. Values MUST NOT be buyer-asserted claims — platforms provide signals based on direct observation or independently verifiable third-party attestations. All signal keys MUST use reverse-domain naming to ensure provenance and prevent collisions when multiple extensions contribute to the shared namespace. + """ + + model_config = ConfigDict( + extra="allow", + ) + dev_ucp_buyer_ip: str | None = Field(None, alias="dev.ucp.buyer_ip") + """ + Client's IP address (IPv4 or IPv6). + """ + dev_ucp_user_agent: str | None = Field(None, alias="dev.ucp.user_agent") + """ + Client's HTTP User-Agent header or equivalent. + """ + + +SignedAmount = TypeAliasType( + "SignedAmount", + Annotated[ + int, + Field( + ..., + ge=-9007199254740991, + le=9007199254740991, + title="Signed Amount", + ), + ], +) +""" +Monetary amount in the currency's minor unit as defined by ISO 4217. Refer to the currency's exponent to determine minor-to-major ratio (e.g., 2 for USD, 0 for JPY, 3 for KWD). May be negative — the sign is intrinsic to the value (e.g., discounts are negative, charges are positive). +""" + + +class Total(BaseModel): + """ + A cost breakdown entry with a category, amount, and optional display text. + """ + + type: str + """ + Cost category. Well-known values: subtotal, items_discount, discount, fulfillment, tax, fee, total. Businesses MAY use additional values. + """ + display_text: str | None = None + """ + Text to display against the amount. Should reflect appropriate method (e.g., 'Shipping', 'Delivery'). + """ + amount: SignedAmount + + +class TotalCreateRequest(BaseModel): + """ + Request payload to create a new Total. A cost breakdown entry with a category, amount, and optional display text. + """ + + +class TotalUpdateRequest(BaseModel): + """ + Request payload to update an existing Total. A cost breakdown entry with a category, amount, and optional display text. + """ + + +class Line(BaseModel): + """ + Sub-line entry. Additional metadata MAY be included. + """ + + display_text: str + """ + Human-readable label for this sub-line. + """ + amount: SignedAmount + + +class Total1(Total): + lines: list[Line] | None = None + """ + Optional itemized breakdown. The parent entry is always rendered; lines are supplementary. Sum of line amounts MUST equal the parent entry amount. + """ + + +class Totals1(BaseModel): + """ + Pricing breakdown provided by the business. MUST contain exactly one subtotal and one total entry. Detail types (tax, fee, discount, fulfillment) may appear multiple times for itemization. Platforms MUST render all entries in order using display_text and amount. + """ + + +Totals = TypeAliasType( + "Totals", Annotated[list[Total1] | Totals1, Field(..., title="Totals")] +) +""" +Pricing breakdown provided by the business. MUST contain exactly one subtotal and one total entry. Detail types (tax, fee, discount, fulfillment) may appear multiple times for itemization. Platforms MUST render all entries in order using display_text and amount. +""" + + +class TotalsCreateRequest1(BaseModel): + """ + Request payload to create a new Totals. Pricing breakdown provided by the business. MUST contain exactly one subtotal and one total entry. Detail types (tax, fee, discount, fulfillment) may appear multiple times for itemization. Platforms MUST render all entries in order using display_text and amount. + """ + + +TotalsCreateRequest = TypeAliasType( + "TotalsCreateRequest", + Annotated[ + list[TotalCreateRequest] | TotalsCreateRequest1, + Field(..., title="TotalsCreateRequest"), + ], +) +""" +Request payload to create a new Totals. Pricing breakdown provided by the business. MUST contain exactly one subtotal and one total entry. Detail types (tax, fee, discount, fulfillment) may appear multiple times for itemization. Platforms MUST render all entries in order using display_text and amount. +""" + + +class TotalsUpdateRequest1(BaseModel): + """ + Request payload to update an existing Totals. Pricing breakdown provided by the business. MUST contain exactly one subtotal and one total entry. Detail types (tax, fee, discount, fulfillment) may appear multiple times for itemization. Platforms MUST render all entries in order using display_text and amount. + """ + + +TotalsUpdateRequest = TypeAliasType( + "TotalsUpdateRequest", + Annotated[ + list[TotalUpdateRequest] | TotalsUpdateRequest1, + Field(..., title="TotalsUpdateRequest"), + ], +) +""" +Request payload to update an existing Totals. Pricing breakdown provided by the business. MUST contain exactly one subtotal and one total entry. Detail types (tax, fee, discount, fulfillment) may appear multiple times for itemization. Platforms MUST render all entries in order using display_text and amount. +""" + + +class Unit(BaseModel): + """ + A reusable unit descriptor for quantities and measures. Its unit-descriptor machine identity is (`unit`, effective `scale`), where effective `scale` is the provided `scale` or 0; `display_text` is excluded. + """ + + unit: str + """ + Stable machine identifier. The Business SHOULD use the exact UN/CEFACT Rec20 Common Code when one accurately identifies the unit. Otherwise, the Business MAY use a custom unit identifier and MUST use it consistently for the same unit. The Platform MUST treat an unrecognized identifier as opaque. + """ + scale: int | None = Field(0, ge=0, le=15) + """ + One step equals `10^-scale` of `unit`. When `unit` is `C62`, `scale`, if present, MUST be 0. The maximum of 15 is derived from the interoperable integer range: at scale 16 a single whole unit (10^16 steps) is no longer representable, so larger scales cannot denominate one unit of their own basis. Businesses needing finer granularity use a smaller unit. + """ + display_text: str + """ + Required printable unit label provided by the Business. The Platform MUST use it when it does not recognize `unit`; for a recognized UN/CEFACT Rec 20 Common Code, the Platform MAY substitute its own localized label. It does not participate in unit identity or mismatch comparison. + """ + + +class ValueConstraint1(BaseModel): + """ + A Value Constraint containing `enum`, `const`, or both. + """ + + model_config = ConfigDict( + extra="forbid", + ) + enum: list[Any] = Field(..., min_length=1) + """ + A non-empty array of unique JSON values. + """ + const: Any | None = None + default: Any | None = None + + +class ValueConstraint2(BaseModel): + """ + A Value Constraint containing `enum`, `const`, or both. + """ + + model_config = ConfigDict( + extra="forbid", + ) + enum: list[Any] | None = Field(None, min_length=1) + """ + A non-empty array of unique JSON values. + """ + const: Any + default: Any | None = None + + +ValueConstraint = TypeAliasType( + "ValueConstraint", ValueConstraint1 | ValueConstraint2 +) +""" +A Value Constraint containing `enum`, `const`, or both. +""" + + +class Barcode(BaseModel): + type: str + """ + Barcode standard. Well-known values: UPC, EAN, ISBN, GTIN, JAN. + """ + value: str + """ + Barcode value. + """ + + +class Seller(BaseModel): + """ + Optional seller context for this variant. + """ + + name: str | None = None + """ + Seller display name. + """ + links: list[Link] | None = None + """ + Seller policy and information links. + """ + + +Version = TypeAliasType( + "Version", Annotated[str, Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$")] +) +""" +Version identifier in YYYY-MM-DD format. +""" + + +class VersionConstraint(BaseModel): + """ + Version range requirement with minimum and optional maximum. + """ + + model_config = ConfigDict( + extra="allow", + ) + min: Version + """ + Minimum required version (inclusive). + """ + max: Version | None = None + """ + Maximum compatible version (inclusive). When absent, no upper bound. + """ + + +WarningCode = TypeAliasType( + "WarningCode", + Annotated[ + str, + Field( + ..., + examples=[ + "final_sale", + "prop65", + "fulfillment_changed", + "payment_term_changed", + "age_restricted", + ], + title="Warning Code", + ), + ], +) +""" +Warning code identifying the type of warning. Standard codes are defined in capability specifications (see examples) and have standardized semantics; freeform codes are permitted. +""" + + +Actions = TypeAliasType( + "Actions", + Annotated[ + dict[ReverseDomainName, list[Instance]], Field(..., title="Actions") + ], +) +""" +Outstanding extension-defined Action instances, keyed by reverse-domain Action type, not extension name. +""" + + +class Allocation(BaseModel): + """ + Breakdown of how a discount amount was allocated to a specific target. + """ + + path: str + """ + RFC 9535 JSONPath to the allocation target (e.g., '$.line_items[0]', '$.totals[?@.type == "fulfillment"]'). + """ + amount: Amount + """ + Amount allocated to this target in ISO 4217 minor units. + """ + + +class AppliedDiscount(BaseModel): + """ + A discount that was successfully applied. + """ + + code: str | None = None + """ + The discount code. Omitted for automatic discounts. + """ + title: str + """ + Human-readable discount name (e.g., 'Summer Sale 20% Off'). + """ + amount: Amount + """ + Total discount amount in ISO 4217 minor units. + """ + automatic: bool | None = False + """ + True if applied automatically by merchant rules (no code required). + """ + method: Literal["each", "across"] | None = None + """ + Allocation method. 'each' = applied independently per item. 'across' = split proportionally by value. + """ + priority: int | None = Field(None, ge=1) + """ + Stacking order for discount calculation. Lower numbers applied first (1 = first). + """ + provisional: bool | None = False + """ + True if this discount requires additional verification. + """ + eligibility: ReverseDomainName | None = None + """ + The eligibility claim accepted by the Business for this discount. Corresponds to a value from context.eligibility. Omitted for code-based and non-eligibility automatic discounts. + """ + allocations: list[Allocation] | None = None + """ + Breakdown of where this discount was allocated. Sum of allocation amounts equals total amount. + """ + + +class CatalogFulfillmentMethod(BaseModel): + """ + A fulfillment method on a catalog variant: how the variant can be fulfilled, and its availability. + """ + + model_config = ConfigDict( + extra="allow", + ) + type: str + """ + Fulfillment method type. Well-known values: `shipping`, `pickup`. Businesses MAY use additional values. + """ + description: Description | None = None + """ + Short buyer-facing summary (e.g. 'Ships in 2–4 business days'). + """ + availability: Availability | None = None + """ + Availability of this variant via this method at the specified or inferred location. + """ + location: str | None = None + """ + Stable, opaque identifier for the Business Location resolved for this place-based fulfillment method. The Business recognizes the same ID when submitted as `selected_destination_id` for that method; recognition does not reserve inventory or guarantee eligibility, and current terms are revalidated. + """ + options: list[FulfillmentOptionBase] | None = None + """ + Representative fulfillment options for this method (e.g. Standard, Express). Without a destination or full cart, a Business SHOULD preview meaningful boundary options (e.g. cheapest, fastest); more specific options are negotiated in Checkout once line items and destination are known. + """ + + +class Config(BaseModel): + """ + Business browser endpoint configuration for shopping permalinks. + """ + + model_config = ConfigDict( + extra="allow", + ) + endpoint: Endpoint + + +class ConsentSegment(BaseModel): + """ + A buyer's consent decision for a specific refinement of a parent purpose (e.g., email marketing under the marketing purpose). Overrides the parent's `granted` value for this scope. Segments do not nest further. + """ + + granted: bool + """ + Whether consent has been granted for this segment. Overrides the parent purpose's `granted` value for this specific scope. + """ + source: Literal["business", "platform"] + """ + Identifies the party that asserted the current `granted` value for this segment. `business` means the value reflects the business's default policy; `platform` means the value reflects an explicit buyer decision captured by the platform. + """ + description: str + """ + Human-readable description of what the buyer is consenting to within this segment (e.g., 'Promotional emails and exclusive offers'). + """ + links: list[Link] | None = None + """ + Optional segment-specific links (e.g., channel terms or privacy disclosures). + """ + + +class ConstraintExpression(BaseModel): + """ + A closed JSON Schema Draft 2020-12 constraint expression with Object and Value Constraint positions. + """ + + model_config = ConfigDict( + extra="forbid", + ) + required: list[str] | None = Field(None, min_length=1) + """ + Property names required by the constrained object. Must be non-empty: an empty array applies no constraint. + """ + properties: dict[str, ConstraintExpression | ValueConstraint] | None = None + """ + Constraints keyed by property name. Must be non-empty: an empty object applies no constraint. + """ + anyOf: list[ConstraintExpression] | None = None + """ + Alternative Object Constraints. The constrained object must satisfy at least one. A branch must be non-empty: an empty branch is satisfied by every object and neutralizes the alternation. + """ + + +class PaymentItem(BaseModel): + handler: ReverseDomainName + """ + Handler registry key advertised in the Business profile's `ucp.payment_handlers`. + """ + types: list[str] | None = None + """ + Optional preferred instrument types for this handler, in priority order, aligned with the handler's advertised `payment_instrument.type` values (for example `card` or `bank`). Unrecognized values MUST be ignored. + """ + + +class Context(Locality): + """ + Provisional buyer signals for relevance and localization—not authoritative data. Businesses SHOULD use these values when verified inputs (e.g., shipping address) are absent, and MAY ignore or down-rank them if inconsistent with higher-confidence signals (authenticated account, risk detection) or regulatory constraints (export controls). Eligibility and policy enforcement MUST occur at checkout time using binding transaction data. Context SHOULD be non-identifying and can be disclosed progressively—coarse signals early, finer resolution as the session progresses. Higher-resolution data (shipping address, billing address) supersedes context. + """ + + model_config = ConfigDict( + extra="allow", + ) + location: str | None = None + """ + Stable, opaque identifier for a Location in the Business's namespace. This provisional, non-binding hint is distinct from the Buyer's locality. The operation specification or an active capability/extension defines its effects. A common example in retail shopping is the default home store ID selected and saved by the user when purchasing groceries. + """ + intent: str | None = None + """ + Background context describing buyer's intent (e.g., 'looking for a gift under $50', 'need something durable for outdoor use'). Informs relevance, recommendations, and personalization. + """ + language: str | None = None + """ + Preferred language for content. Use IETF BCP 47 language tags (e.g., 'en', 'fr-CA', 'zh-Hans'). For REST, equivalent to Accept-Language header—platforms SHOULD fall back to Accept-Language when this field is absent; when provided, overrides Accept-Language. Businesses MAY return content in a different language if unavailable. + """ + currency: str | None = None + """ + Preferred currency (ISO 4217, e.g., 'EUR', 'USD'). Businesses determine presentment currency from context and authoritative signals; this hint MAY inform selection in multi-currency markets. Also serves as the denomination for price filter values — platforms SHOULD include this field when sending price filters. Response prices include explicit currency confirming the resolution. + """ + eligibility: list[ReverseDomainName] | None = None + """ + Buyer claims about eligible benefits such as loyalty membership, payment instrument perks, and similar. Recognized claims MAY inform the Business response (e.g., member-only product availability, adjusted pricing in catalog, provisional discounts at cart or checkout). Businesses MUST ignore unrecognized values without error. Values MUST use reverse-domain naming (e.g., 'com.example.loyalty_gold', 'org.school.student') and MUST be non-identifying. + """ + payment: list[PaymentItem] | None = None + """ + Buyer-preferred payment handlers in priority order (most preferred first). Each entry names a handler advertised in the Business profile's `ucp.payment_handlers`, optionally narrowed to preferred instrument types. The Business SHOULD use it to preselect or prioritize the handler (and type, when given) and MAY ignore unavailable or ineligible entries; unrecognized values MUST be ignored without error. + """ + + +class DetailOptionValue(OptionValue): + """ + An option value with availability signals relative to the current selections. Used in get_product responses where selected context exists. + """ + + available: bool | None = None + """ + Whether a variant matching this value and the current option selections is purchasable. + """ + exists: bool | None = None + """ + Whether a variant matching this value and the current option selections exists in the catalog. + """ + + +class Option(BaseModel): + name: str + values: list[DetailOptionValue] = Field(..., min_length=1) + + +class DiscountsObject(BaseModel): + """ + Discount codes input and applied discounts output. + """ + + codes: list[str] | None = None + """ + Discount codes to apply. Case-insensitive. Replaces previously submitted codes. Send empty array to clear. + """ + applied: list[AppliedDiscount] | None = None + """ + Discounts successfully applied (code-based and automatic). + """ + + +class Expectation(BaseModel): + """ + Buyer-facing fulfillment expectation representing logical groupings of items (e.g., 'package'). Can be split, merged, or adjusted post-order to set buyer expectations for when/how items arrive. + """ + + id: str + """ + Expectation identifier. + """ + line_items: list[LineItem1] + """ + Which line items and quantities are in this expectation. + """ + method_type: str + """ + Delivery method type. Well-known values: `shipping`, `pickup`, `digital`; additional values MAY be used. + """ + destination: PostalAddress + """ + Delivery destination address. + """ + description: str | None = None + """ + Human-readable delivery description (e.g., 'Arrives in 5-8 business days'). + """ + fulfillable_on: str | None = None + """ + When this expectation can be fulfilled: 'now' or ISO 8601 timestamp for future date (backorder, pre-order). + """ + + +class FulfillmentCreateRequest(BaseModel): + """ + Request payload to create a new Fulfillment. Container for fulfillment methods and availability. + """ + + methods: list[FulfillmentMethodCreateRequest] | None = None + """ + Fulfillment methods for cart items. + """ + + +class FulfillmentDestinationFilter(Locality): + """ + A specific destination, named by value or by reference: a coarse locality (`address_country` / `address_region` / `postal_code`), or a `location` id. Platforms SHOULD provide one or the other, not both; if both are present, a business SHOULD use the more specific — typically `location`. + """ + + model_config = ConfigDict( + extra="allow", + ) + location: str | None = None + """ + A reference to the destination (e.g. store, pickup location, saved address). + """ + + +class FulfillmentOption(FulfillmentOptionBase): + """ + A fulfillment option within a group (e.g., Standard Shipping $5, Express $15). Extends the fulfillment option base with cost and timing. + """ + + carrier: str | None = None + """ + Carrier name (for shipping). + """ + earliest_fulfillment_time: AwareDatetime | None = None + """ + Earliest fulfillment date. + """ + latest_fulfillment_time: AwareDatetime | None = None + """ + Latest fulfillment date. + """ + totals: list[Total] + """ + Fulfillment option totals breakdown. + """ + + +class FulfillmentSearchFilters(SearchFilters): + """ + Catalog filters extended with a fulfillment destination filter and a method-type filter. + """ + + fulfills_to: FulfillmentDestinationFilter | None = None + """ + Explicit destination where items are fulfilled. It may differ from the locality or Business Location supplied in `context` (e.g. a gift delivered directly to the recipient). The filter restricts results to what can be fulfilled there and seeds method `availability`. It supersedes `context` only for fulfillment destination and availability resolution. + """ + methods: list[str] | None = None + """ + Restrict results to these fulfillment method types (e.g. ["pickup"]). Well-known values: `shipping`, `pickup`. + """ + + +class GetProductRequest(BaseModel): + """ + Request body for single-product retrieval. Supports interactive variant narrowing via selected and preferences. + """ + + id: str + """ + Product or variant identifier. Implementations MUST support product ID and variant ID. + """ + selected: list[SelectedOption] | None = None + """ + Partial or full option selections for interactive variant narrowing. When provided, response option values include availability signals (available, exists) relative to these selections. + """ + preferences: list[str] | None = None + """ + Option names in relaxation priority order. When no exact variant matches all selections, the server drops options from the end of this list first. E.g., ['Color', 'Size'] keeps Color and relaxes Size. + """ + filters: SearchFilters | None = None + """ + Filter criteria to narrow returned variants. All specified filters combine with AND logic. + """ + context: Context | None = None + signals: Signals | None = None + attribution: Attribution | None = None + + +class LocationDestinationCreateRequest(LocationSummaryCreateRequest): + """ + Request payload to create a new LocationDestination. A business location fulfillment destination. Business-authored and response-only: the Platform selects a location via `selected_destination_id` rather than writing destinations. + """ + + type: Literal["business_location"] = "business_location" + + +class LocationDestinationUpdateRequest(LocationSummaryUpdateRequest): + """ + Request payload to update an existing LocationDestination. A business location fulfillment destination. Business-authored and response-only: the Platform selects a location via `selected_destination_id` rather than writing destinations. + """ + + type: Literal["business_location"] = "business_location" + + +class LocationSummary(BaseModel): + """ + A summary of a physical business location. + """ + + model_config = ConfigDict( + extra="allow", + ) + id: str + """ + Stable, opaque, Business-scoped Location identifier. + """ + name: str + """ + Buyer-facing, Business-owned display name. + """ + address: PostalAddress | None = None + """ + Physical address of the location. + """ + + +class LookupRequest(BaseModel): + """ + Request body for catalog lookup. + """ + + ids: list[str] = Field(..., min_length=1) + """ + Identifiers to lookup. Implementations MUST support product ID and variant ID; MAY support secondary identifiers (SKU, handle, etc.). + """ + filters: SearchFilters | None = None + """ + Filter criteria to narrow returned products and variants. All specified filters combine with AND logic. + """ + context: Context | None = None + signals: Signals | None = None + attribution: Attribution | None = None + + +class Measure(Unit): + """ + A measure composed of an integer value and a unit descriptor. Its value is the integer count of `10^-scale` units of `unit`. + """ + + value: int = Field(..., ge=-9007199254740991, le=9007199254740991) + """ + Integer count of `10^-scale` units of `unit`. + """ + + +class MessageWarning(BaseModel): + type: Literal["warning"] = "warning" + """ + Message type discriminator. + """ + path: str | None = None + """ + RFC 9535 JSONPath to the component the message refers to (e.g., $.line_items[0]). + """ + code: WarningCode + content: str + """ + Human-readable warning message that MUST be displayed. + """ + content_type: Literal["plain", "markdown"] | None = "plain" + """ + Content format, default = plain. + """ + presentation: str | None = "notice" + """ + Rendering contract for this warning. 'notice' (default): platform MUST display, MAY dismiss. 'disclosure': platform MUST display in proximity to the path-referenced component, MUST NOT hide or auto-dismiss. See specification for full contract. + """ + image_url: AnyUrl | None = None + """ + URL to a required visual element (e.g., warning symbol, energy class label). + """ + url: AnyUrl | None = None + """ + Reference URL for more information (e.g., regulatory site, registry entry, policy page). + """ + + +class Fulfillment1(BaseModel): + """ + Fulfillment data: buyer expectations and what actually happened. + """ + + expectations: list[Expectation] | None = None + """ + Buyer-facing groups representing when/how items will be delivered. Can be split, merged, or adjusted post-order. + """ + events: list[FulfillmentEvent] | None = None + """ + Append-only event log of actual shipments. Each event references line items by ID. + """ + + +class PaymentInstrument(BaseModel): + """ + The base definition for any payment instrument. It links the instrument to a specific payment handler. + """ + + model_config = ConfigDict( + extra="allow", + ) + id: str + """ + A unique identifier for this instrument instance. Typically assigned by the platform for instruments it collects. For a business-owned saved instrument returned on an identity-linked response, this identifier is assigned by the business; the platform MUST treat it as an opaque, business-scoped reference, and the business resolves it server-side when the buyer selects it. + """ + handler_id: str + """ + The unique identifier for the handler instance that produced this instrument. This corresponds to the 'id' field in the Payment Handler definition. + """ + type: str + """ + The broad category of the instrument (e.g., 'card', 'tokenized_card'). Specific schemas will constrain this to a constant value. + """ + billing_address: PostalAddress | None = None + """ + The billing address associated with this payment method. + """ + credential: PaymentCredential | None = None + display: dict[str, Any] | None = None + """ + Display information for this payment instrument. Each payment instrument schema defines its specific display properties, as outlined by the payment handler. + """ + + +class Policy(BaseModel): + """ + A durable business rule about the items in a response — return/refund terms, warranty, and the like — at the time of purchase. Every policy carries a `type` (an open reverse-DNS vocabulary) and a `description` so a platform can present it without understanding its type-specific fields; type-specific fields (gated by `type`) add structured context for platforms that model that type. Policies are reference data; the obligation to display a term to the buyer is carried by a `messages[]` warning whose `code` equals the policy `type` — see the Policies section of the specification. + """ + + model_config = ConfigDict( + extra="allow", + ) + type: ReverseDomainName + """ + Policy type discriminator. Open reverse-DNS vocabulary. Well-known values: `dev.ucp.shopping.policy.return` (return terms), `dev.ucp.shopping.policy.warranty` (warranty terms). Businesses MAY define custom types in their own domain (e.g., `com.example.policy.price_match`). Platforms MUST tolerate unknown values. + """ + description: Description + """ + Human-readable policy summary in one or more formats (plain, markdown, html). Required on every policy so a platform can present it without understanding any type-specific fields. This is not the buyer-facing disclosure — display is compelled by a `messages[]` warning (see the Policies section). + """ + applies_to: list[str] | None = None + """ + RFC 9535 JSONPath expressions identifying the nodes this policy applies to, relative to the embedding response root (e.g., `$.line_items[0]` in cart/checkout, `$.products[2]` in catalog). Each target covers the node it names and everything nested under it, so a target on a product also covers its variants. A singular query (RFC 9535 Section 2.3.5.1; name and index selectors only) names a single node; filters, wildcards, and slices match a set. When omitted, the policy applies to the entire response. When policies of the same `type` contest a node, the narrowest target wins and overrides the rest. See the Policies section for how specificity resolves. + """ + url: AnyUrl | None = None + """ + Optional link to the full policy document. + """ + + +class QuantityUnit(Unit): + """ + Sale-basis descriptor for quantities: the shared unit descriptor plus the Business's ordering policy. Its unit-descriptor machine identity remains (`unit`, effective `scale`); `display_text` and `increment` are excluded from identity and mismatch comparison. + """ + + increment: int | None = Field(1, ge=1) + """ + Ordering granularity, denominated in steps: the Business sells this item in integer multiples of `increment` steps. Its effective value is the provided value or 1. Advisory merchandising policy, not a representational bound: Platform-authored quantities SHOULD be integer multiples of the effective increment; the Business MAY accept, revise, or reject an off-increment request with a recoverable business outcome and MUST NOT silently reinterpret it. Business-authored quantities (checkout revisions, fulfillment events, adjustments) are bounded only by `scale`. + """ + + +class RequestConstraints(BaseModel): + """ + Binds the shared Constraint Expression grammar to data in the next UCP request to the same resource. + """ + + model_config = ConfigDict( + extra="forbid", + ) + path: str | None = None + """ + A complete RFC 9535 JSONPath query evaluated against the next logical UCP request to the same resource. + """ + required: list[str] | None = Field(None, min_length=1) + """ + Property names required by the constrained object. Must be non-empty: an empty array applies no constraint. + """ + properties: dict[str, ConstraintExpression | ValueConstraint] | None = ( + Field(None, min_length=1) + ) + """ + Constraints keyed by property name. Must be non-empty: an empty object applies no constraint. + """ + anyOf: list[ConstraintExpression] | None = Field(None, min_length=1) + """ + Alternative Object Constraints. The constrained object must satisfy at least one. A branch must be non-empty: an empty branch is satisfied by every object and neutralizes the alternation. + """ + + +class Requires(BaseModel): + """ + Version requirements for extension schemas. Declares minimum (and optionally maximum) protocol and capability versions needed for correct operation. + """ + + model_config = ConfigDict( + extra="allow", + ) + protocol: VersionConstraint | None = None + """ + Required range for the selected `ucp.version`. + """ + capabilities: dict[ReverseDomainName, VersionConstraint] | None = None + """ + Required capability versions, keyed by capability name. Keys must be a subset of the extension's $defs keys. + """ + + +class SearchRequest(BaseModel): + query: str | None = None + """ + Free-text search query. + """ + context: Context | None = None + signals: Signals | None = None + attribution: Attribution | None = None + filters: SearchFilters | None = None + pagination: Request | None = None + + +class SelectedPaymentInstrument(PaymentInstrument): + """ + A payment instrument with selection state. + """ + + selected: bool | None = None + """ + Whether this instrument is selected by the user. + """ + + +class UcpEntity(BaseModel): + """ + Shared foundation for all UCP entities. + """ + + version: Version + """ + Entity version in YYYY-MM-DD format. + """ + spec: AnyUrl | None = None + """ + URL to human-readable specification document. + """ + schema_: AnyUrl | None = Field(None, alias="schema") + """ + URL to JSON Schema defining this entity's structure and payloads. + """ + id: str | None = None + """ + Unique identifier for this entity instance. Used to disambiguate when multiple instances exist. + """ + config: dict[str, Any] | None = None + """ + Entity-specific configuration. Structure defined by each entity's schema. + """ + + +class Measure1(Measure): + """ + Product quantity in packaging/content (for example, a 750 mL bottle), distinct from `quantity_unit`, which defines the sale basis. Its integer `value` MUST be at least 1. + """ + + value: Any | None = Field(None, ge=1) + + +class Reference(Measure): + """ + Denominator for unit price display (for example, per 100 mL or per 1 kg). Its integer `value` MUST be at least 1. + """ + + value: Any | None = Field(None, ge=1) + + +class UnitPrice(BaseModel): + """ + Price per standard unit of measurement. MAY be omitted when unit pricing does not apply. `unit_price.currency` MUST equal `price.currency`; the comparator MUST NOT perform currency conversion. `measure.unit` and `reference.unit` MUST be identical; cross-unit conversion is not permitted. Their scales MAY differ; each value represents `value × 10^-scale`. + """ + + amount: Amount + """ + Unit price in ISO 4217 minor units. After satisfying the same-unit invariant, the Business MUST compute the comparator as `(price.amount / (measure.value × 10^-measure.scale)) × (reference.value × 10^-reference.scale)` and round it once to ISO 4217 minor units according to its pricing rules. The returned `unit_price.amount` is authoritative; the Platform MUST NOT recompute or substitute its own result. + """ + currency: str = Field(..., pattern="^[A-Z]{3}$") + """ + ISO 4217 currency code. + """ + measure: Measure1 + """ + Product quantity in packaging/content (for example, a 750 mL bottle), distinct from `quantity_unit`, which defines the sale basis. Its integer `value` MUST be at least 1. + """ + reference: Reference + """ + Denominator for unit price display (for example, per 100 mL or per 1 kg). Its integer `value` MUST be at least 1. + """ + + +class Variant(BaseModel): + """ + A purchasable variant of a product with specific option selections. + """ + + id: str + """ + Global ID (GID) uniquely identifying this variant. Used as item.id in checkout. + """ + sku: str | None = None + """ + Business-assigned identifier for inventory and fulfillment. + """ + barcodes: list[Barcode] | None = None + """ + Industry-standard product identifiers for cross-reference and correlation. + """ + handle: str | None = None + """ + URL-safe variant handle/slug. + """ + title: str + """ + Variant display title (e.g., 'Blue / Large'). + """ + description: Description + """ + Variant description in one or more formats. + """ + url: AnyUrl | None = None + """ + Canonical variant page URL. + """ + categories: list[Category] | None = None + """ + Variant categories with optional taxonomy identifiers. + """ + price: Price + """ + Current selling price. Price is the amount per one whole `quantity_unit.unit` (for example, per lb or per hour); when `quantity_unit` is absent, it is per `each`. Line total is `price × quantity × 10^-scale`, computed and rounded once by the Business; `totals` remain authoritative. + """ + quantity_unit: QuantityUnit | None = None + """ + Sale basis this variant's `quantity` is denominated in. The default sale basis is `each`, whose machine identity is (`C62`, 0); `C62` is the UN/CEFACT Rec20 code for one/each. An absent catalog descriptor encodes that default. An `increment` advertises the ordering granularity in steps (for example, `scale` 2 with `increment` 25 sells in 0.25-unit multiples). + """ + list_price: Price | None = None + """ + List price before discounts (for strikethrough display). + """ + unit_price: UnitPrice | None = None + """ + Price per standard unit of measurement, for shelf-style comparison display. MAY be omitted when unit pricing does not apply. + """ + availability: Availability | None = None + """ + Variant availability for purchase. + """ + options: list[SelectedOption] | None = None + """ + Option values that define this variant (e.g., Color: Blue, Size: Large). + """ + media: list[Media] | None = None + """ + Variant media (images, videos, 3D models). First item is the featured media for listings. + """ + rating: Rating | None = None + """ + Variant rating. + """ + tags: list[str] | None = None + """ + Variant tags for categorization and search. + """ + metadata: dict[str, Any] | None = None + """ + Business-defined custom data extending the standard variant model. + """ + seller: Seller | None = None + """ + Optional seller context for this variant. + """ + + +class LineItem(BaseModel): + id: str + """ + Line item ID reference. + """ + quantity: int = Field(..., ge=-9007199254740991, le=9007199254740991) + """ + Signed integer count of steps of the referenced line item's `quantity_unit` (`10^-scale` × `unit`); when `quantity_unit` is absent, it counts whole items (`each`). Negative values represent reductions (e.g. returns); positive values represent additions (e.g. exchanges). + """ + measure: Measure | None = None + """ + The settled measurement this adjustment reconciles (for example, actual picked weight), present when the line's price settles by measurement. Its unit identity MUST match the line's pricing basis (`item.unit_price` measure/reference unit); no unit conversion. A pure price settlement uses `quantity: 0` together with `measure` and a totals delta. + """ + + +class Adjustment(BaseModel): + """ + Post-order event that exists independently of fulfillment. Typically represents money movements but can be any post-order change. Polymorphic type that can optionally reference line items. + """ + + id: str + """ + Adjustment event identifier. + """ + type: str + """ + Type of adjustment (open string). Typically money-related like: refund, return, credit, price_adjustment, dispute, cancellation. Can be any value that makes sense for the merchant's business. + """ + occurred_at: AwareDatetime + """ + RFC 3339 timestamp when this adjustment occurred. + """ + status: Literal["pending", "completed", "failed"] + """ + Adjustment status. + """ + line_items: list[LineItem] | None = None + """ + Which line items and quantities are affected (optional). + """ + totals: list[Total] | None = None + """ + Adjustment totals breakdown. Signed values - negative for money returned to buyer (refunds, credits), positive for additional charges (exchanges). + """ + description: str | None = None + """ + Human-readable reason or description (e.g., 'Defective item', 'Customer requested'). + """ + + +class AvailablePaymentInstrument(BaseModel): + """ + An instrument type available from a payment handler with optional constraints. + """ + + type: str + """ + The instrument type identifier (e.g., 'card', 'gift_card'). References an instrument schema's type constant. + """ + constraints: ConstraintExpression | None = None + """ + A Constraint Expression describing the instrument this entry makes available. Keys in `properties` name members of the `constraint_target` declared by the instrument schema for this `type`. Requirements on submitted request data belong in `ucp.request_constraints` instead. + """ + + +class CapabilityBase(UcpEntity): + extends: list[ReverseDomainName] | None = None + """ + Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. + """ + + +class CapabilityBusinessSchema(CapabilityBase): + """ + Capability declaration for business/merchant discovery. Requires the `schema` URL so platforms can fetch and compose it during negotiation; may also include business-specific config overrides. + """ + + +class CapabilityPlatformSchema(CapabilityBase): + """ + Full capability declaration for platform-level discovery. Includes spec/schema URLs for agent fetching. + """ + + +class CapabilityResponseSchema(CapabilityBase): + """ + Capability reference in responses. Only name/version required to confirm active capabilities. + """ + + +class CatalogFulfillment(BaseModel): + """ + How a catalog variant can be fulfilled. Mirrors checkout `fulfillment`. + """ + + model_config = ConfigDict( + extra="allow", + ) + methods: list[CatalogFulfillmentMethod] | None = None + """ + Fulfillment methods for this variant. + """ + + +class ConsentPurpose(BaseModel): + """ + A buyer's consent decision for a purpose (e.g., marketing, analytics). Carries the current binary state, its source (business default or platform-captured buyer decision), human-readable context, and optional refinements scoping the decision to specific channels, vendors, or programs. + """ + + granted: bool + """ + Whether consent has been granted for this purpose. The `source` field identifies who asserted this state (business default or platform-captured buyer preference). + """ + source: Literal["business", "platform"] + """ + Identifies the party that asserted the current `granted` value. `business` means the value reflects the business's default policy; `platform` means the value reflects an explicit buyer decision captured by the platform. + """ + description: str + """ + Human-readable description of what the buyer is consenting to (e.g., 'Promotional communications across all channels'). + """ + links: list[Link] | None = None + """ + Optional links providing context (e.g., privacy policy, terms). + """ + segments: dict[ReverseDomainName, ConsentSegment] | None = None + """ + Optional refinements scoping this purpose to specific channels, vendors, or programs. Keys are reverse-DNS identifiers. UCP currently defines two well-known segment identifiers under `dev.ucp.consent.marketing`: `dev.ucp.consent.marketing.email`, `dev.ucp.consent.marketing.sms`. Other segments follow vendor or merchant reverse-DNS conventions. + """ + + +FulfillmentDestinationCreateRequest = TypeAliasType( + "FulfillmentDestinationCreateRequest", + Annotated[ + LocationDestinationCreateRequest | ShippingDestinationCreateRequest, + Field( + ..., + discriminator="type", + title="FulfillmentDestinationCreateRequest", + ), + ], +) +""" +Request payload to create a new FulfillmentDestination. A destination for fulfillment. +""" + + +FulfillmentDestinationUpdateRequest = TypeAliasType( + "FulfillmentDestinationUpdateRequest", + Annotated[ + LocationDestinationUpdateRequest | ShippingDestinationUpdateRequest, + Field( + ..., + discriminator="type", + title="FulfillmentDestinationUpdateRequest", + ), + ], +) +""" +Request payload to update an existing FulfillmentDestination. A destination for fulfillment. +""" + + +class FulfillmentGetProductRequest(GetProductRequest): + filters: FulfillmentSearchFilters | None = None + """ + Filter criteria to narrow returned variants. All specified filters combine with AND logic. + """ + + +class FulfillmentGroup(BaseModel): + """ + A merchant-generated package/group of line items with fulfillment options. + """ + + model_config = ConfigDict( + extra="allow", + ) + id: str + """ + Group identifier for referencing merchant-generated groups in updates. + """ + line_item_ids: list[str] + """ + Line item IDs included in this group/package. + """ + options: list[FulfillmentOption] | None = None + """ + Available fulfillment options for this group. + """ + selected_option_id: str | None = None + """ + ID of the selected fulfillment option for this group. + """ + + +class FulfillmentLookupRequest(LookupRequest): + filters: FulfillmentSearchFilters | None = None + """ + Filter criteria to narrow returned products and variants. All specified filters combine with AND logic. + """ + + +class FulfillmentSearchRequest(SearchRequest): + filters: FulfillmentSearchFilters | None = None + + +class FulfillmentVariant(Variant): + """ + A catalog variant with fulfillment. + """ + + fulfillment: CatalogFulfillment | None = None + + +class Item(BaseModel): + id: str + """ + The product identifier, often the SKU, required to resolve the product details associated with this line item. Should be recognized by both the Platform, and the Business. + """ + title: str + """ + Product title. + """ + price: Amount + """ + Unit price in ISO 4217 minor units. Price is the amount per one whole `quantity_unit.unit` (for example, per lb or per hour); when `quantity_unit` is absent, it is per `each`. + """ + quantity_unit: QuantityUnit | None = None + """ + Sale basis this item's `quantity` is denominated in. On an authoritative Business response, absence encodes the default `each` machine identity (`C62`, 0); the Business MUST include this descriptor for every non-`each` response. On Platform requests, omission makes no assertion: the Business interprets `quantity` using the item's authoritative sale basis. If the Platform includes this descriptor, it asserts the unit-descriptor machine identity. The Business MUST compare that machine identity (`unit`, effective `scale`), ignore `display_text` and `increment`, and resolve a mismatch by conversion surfaced as a visible line revision with a warning, or by rejection with a recoverable business outcome; silent reinterpretation is forbidden. An explicit `C62` descriptor at effective scale 0 matches an authoritative basis represented by an absent descriptor. + """ + unit_price: UnitPrice | None = None + """ + Pricing basis for this item. On an authoritative Business response, the Business MUST include `unit_price` on every line whose pricing basis differs from its sale basis (for example, priced per pound but sold per `each`); presence on a line marks the rate as transactional rather than display-only. When the pricing basis is the sale basis, `item.price` fully denominates the charge and this field MAY be omitted. + """ + image_url: AnyUrl | None = None + """ + Product image URI. + """ + + +class ItemCreateRequest(BaseModel): + """ + Request payload to create a new Item. + """ + + id: str + """ + The product identifier, often the SKU, required to resolve the product details associated with this line item. Should be recognized by both the Platform, and the Business. + """ + quantity_unit: QuantityUnit | None = None + """ + Sale basis this item's `quantity` is denominated in. On an authoritative Business response, absence encodes the default `each` machine identity (`C62`, 0); the Business MUST include this descriptor for every non-`each` response. On Platform requests, omission makes no assertion: the Business interprets `quantity` using the item's authoritative sale basis. If the Platform includes this descriptor, it asserts the unit-descriptor machine identity. The Business MUST compare that machine identity (`unit`, effective `scale`), ignore `display_text` and `increment`, and resolve a mismatch by conversion surfaced as a visible line revision with a warning, or by rejection with a recoverable business outcome; silent reinterpretation is forbidden. An explicit `C62` descriptor at effective scale 0 matches an authoritative basis represented by an absent descriptor. + """ + + +class ItemUpdateRequest(BaseModel): + """ + Request payload to update an existing Item. + """ + + id: str + """ + The product identifier, often the SKU, required to resolve the product details associated with this line item. Should be recognized by both the Platform, and the Business. + """ + quantity_unit: QuantityUnit | None = None + """ + Sale basis this item's `quantity` is denominated in. On an authoritative Business response, absence encodes the default `each` machine identity (`C62`, 0); the Business MUST include this descriptor for every non-`each` response. On Platform requests, omission makes no assertion: the Business interprets `quantity` using the item's authoritative sale basis. If the Platform includes this descriptor, it asserts the unit-descriptor machine identity. The Business MUST compare that machine identity (`unit`, effective `scale`), ignore `display_text` and `increment`, and resolve a mismatch by conversion surfaced as a visible line revision with a warning, or by rejection with a recoverable business outcome; silent reinterpretation is forbidden. An explicit `C62` descriptor at effective scale 0 matches an authoritative basis represented by an absent descriptor. + """ + + +class LineItemModel(BaseModel): + """ + Line item object. Expected to use the currency of the parent object. + """ + + id: str + item: Item + quantity: int = Field(..., ge=1, le=9007199254740991) + """ + Always an integer step count. On Platform requests, steps use the item's Business-authoritative sale basis; omitting `item.quantity_unit` makes no assertion and does not imply `each`. On Business responses, `item.quantity_unit` describes the basis; if absent, it encodes the `each` machine identity (`C62`, 0) and `quantity` counts whole items. + """ + totals: list[Total] + """ + Line item totals breakdown. + """ + parent_id: str | None = None + """ + Parent line item identifier for any nested structures. + """ + + +class LineItemCreateRequest(BaseModel): + """ + Request payload to create a new LineItem. Line item object. Expected to use the currency of the parent object. + """ + + item: ItemCreateRequest + quantity: int = Field(..., ge=1, le=9007199254740991) + """ + Always an integer step count. On Platform requests, steps use the item's Business-authoritative sale basis; omitting `item.quantity_unit` makes no assertion and does not imply `each`. On Business responses, `item.quantity_unit` describes the basis; if absent, it encodes the `each` machine identity (`C62`, 0) and `quantity` counts whole items. + """ + + +class LineItemUpdateRequest(BaseModel): + """ + Request payload to update an existing LineItem. Line item object. Expected to use the currency of the parent object. + """ + + id: str | None = None + item: ItemUpdateRequest + quantity: int = Field(..., ge=1, le=9007199254740991) + """ + Always an integer step count. On Platform requests, steps use the item's Business-authoritative sale basis; omitting `item.quantity_unit` makes no assertion and does not imply `each`. On Business responses, `item.quantity_unit` describes the basis; if absent, it encodes the `each` machine identity (`C62`, 0) and `quantity` counts whole items. + """ + parent_id: str | None = None + """ + Parent line item identifier for any nested structures. + """ + + +class LocationDestination(LocationSummary): + """ + A business location fulfillment destination. Business-authored and response-only: the Platform selects a location via `selected_destination_id` rather than writing destinations. + """ + + type: Literal["business_location"] = "business_location" + """ + Destination type discriminator. Response-only. + """ + + +class LookupVariant(Variant): + """ + Variant with required correlation metadata for lookup responses. + """ + + inputs: list[InputCorrelation] = Field(..., min_length=1) + """ + Which request identifiers resolved to this variant, and how. Each entry maps a request ID to its match type. + """ + + +class Members(BaseModel): + """ + Members defined inside the reserved `ucp` protocol object. The object is open for forward compatibility: consumers MUST ignore unrecognized members. Only UCP core defines members, and every defined member MUST be safe to ignore. + """ + + model_config = ConfigDict( + extra="allow", + ) + map_order: MapOrder | None = None + request_constraints: RequestConstraints | None = None + + +Message = TypeAliasType( + "Message", + Annotated[ + MessageError | MessageWarning | MessageInfo, + Field(..., discriminator="type", title="Message"), + ], +) +""" +Container for error, warning, or info messages. +""" + + +class OrderLineItem(BaseModel): + id: str + """ + Line item identifier. + """ + item: Item + """ + Purchased item data, including identity, price, and sale basis. + """ + quantity: Quantity + """ + Tracks the line item's original, current active, and fulfilled quantities. All three values use the same inherited `item.quantity_unit`. When `item.quantity_unit` is absent on an authoritative order response, each step is one whole item (`each`) under the shared default. + """ + totals: list[Total] + """ + Line item totals breakdown. + """ + status: Literal["processing", "partial", "fulfilled", "removed"] + """ + Derived status: removed if quantity.total == 0, fulfilled if quantity.total > 0 and quantity.fulfilled == quantity.total, partial if quantity.total > 0 and quantity.fulfilled > 0, otherwise processing. + """ + parent_id: str | None = None + """ + Parent line item identifier for any nested structures. + """ + + +class Payment(BaseModel): + """ + Payment configuration containing handlers. + """ + + instruments: list[SelectedPaymentInstrument] | None = None + """ + The payment instruments available for this payment. Each instrument is associated with a specific handler via the handler_id field. Handlers can extend the base payment_instrument schema to add handler-specific fields. + """ + + +class PaymentHandlerBase(UcpEntity): + available_instruments: list[AvailablePaymentInstrument] | None = Field( + None, min_length=1 + ) + """ + Instrument types this handler supports, with optional constraints. When absent, every instrument should be considered available. + """ + + +class PaymentHandlerBusinessSchema(PaymentHandlerBase): + """ + Business declaration for discovery profiles. May include partial config state required for discovery. + """ + + +class PaymentHandlerPlatformSchema(PaymentHandlerBase): + """ + Platform declaration for discovery profiles. May include partial config state required for discovery. + """ + + +class PaymentHandlerResponseSchema(PaymentHandlerBase): + """ + Handler reference in responses. May include full config state for runtime usage of the handler. + """ + + +class Product(BaseModel): + """ + A product in the catalog with variants and options. + """ + + id: str + """ + Global ID (GID) uniquely identifying this product. + """ + handle: str | None = None + """ + URL-safe slug for SEO-friendly URLs (e.g., 'blue-runner-pro'). Use id for stable API references. + """ + title: str + """ + Product title. + """ + description: Description + """ + Product description in one or more formats. + """ + url: AnyUrl | None = None + """ + Canonical product page URL. + """ + categories: list[Category] | None = None + """ + Product categories with optional taxonomy identifiers. + """ + price_range: PriceRange + """ + Price range across all variants. + """ + list_price_range: PriceRange | None = None + """ + List price range before discounts (for strikethrough display). + """ + media: list[Media] | None = None + """ + Product media (images, videos, 3D models). First item is the featured media for listings. + """ + options: list[ProductOption] | None = None + """ + Product options (Size, Color, etc.). + """ + variants: list[Variant] = Field(..., min_length=1) + """ + Purchasable variants of this product. First item is the featured variant for listings. + """ + rating: Rating | None = None + """ + Aggregate product rating. + """ + tags: list[str] | None = None + """ + Product tags for categorization and search. + """ + metadata: dict[str, Any] | None = None + """ + Business-defined custom data extending the standard product model. + """ + + +class ServiceBase(UcpEntity): + transport: Literal["rest", "mcp", "a2a", "embedded"] + """ + Transport protocol for this service binding. + """ + endpoint: AnyUrl | None = None + """ + Endpoint URL for this transport binding. + """ + + +class ServiceBusinessSchema5(ServiceBase): + """ + Service binding for business/merchant configuration. May override platform endpoints. + """ + + +class ServiceBusinessSchema6(ServiceBusinessSchema1, ServiceBusinessSchema5): + """ + Service binding for business/merchant configuration. May override platform endpoints. + """ + + +class ServiceBusinessSchema7(ServiceBusinessSchema2, ServiceBusinessSchema5): + """ + Service binding for business/merchant configuration. May override platform endpoints. + """ + + +class ServiceBusinessSchema8(ServiceBusinessSchema3, ServiceBusinessSchema5): + """ + Service binding for business/merchant configuration. May override platform endpoints. + """ + + +class ServiceBusinessSchema9(ServiceBusinessSchema4, ServiceBusinessSchema5): + """ + Service binding for business/merchant configuration. May override platform endpoints. + """ + + +ServiceBusinessSchema = TypeAliasType( + "ServiceBusinessSchema", + Annotated[ + ServiceBusinessSchema6 + | ServiceBusinessSchema7 + | ServiceBusinessSchema8 + | ServiceBusinessSchema9, + Field(..., title="Service (Business Schema)"), + ], +) +""" +Service binding for business/merchant configuration. May override platform endpoints. +""" + + +class ServicePlatformSchema5(ServiceBase): + """ + Full service declaration for platform-level discovery. All transports require `version`, `spec`, and `transport`. REST, MCP, and embedded additionally require `schema`. + """ + + +class ServicePlatformSchema6(ServicePlatformSchema1, ServicePlatformSchema5): + """ + Full service declaration for platform-level discovery. All transports require `version`, `spec`, and `transport`. REST, MCP, and embedded additionally require `schema`. + """ + + +class ServicePlatformSchema7(ServicePlatformSchema2, ServicePlatformSchema5): + """ + Full service declaration for platform-level discovery. All transports require `version`, `spec`, and `transport`. REST, MCP, and embedded additionally require `schema`. + """ + + +class ServicePlatformSchema8(ServicePlatformSchema3, ServicePlatformSchema5): + """ + Full service declaration for platform-level discovery. All transports require `version`, `spec`, and `transport`. REST, MCP, and embedded additionally require `schema`. + """ + + +class ServicePlatformSchema9(ServicePlatformSchema4, ServicePlatformSchema5): + """ + Full service declaration for platform-level discovery. All transports require `version`, `spec`, and `transport`. REST, MCP, and embedded additionally require `schema`. + """ + + +ServicePlatformSchema = TypeAliasType( + "ServicePlatformSchema", + Annotated[ + ServicePlatformSchema6 + | ServicePlatformSchema7 + | ServicePlatformSchema8 + | ServicePlatformSchema9, + Field(..., title="Service (Platform Schema)"), + ], +) +""" +Full service declaration for platform-level discovery. All transports require `version`, `spec`, and `transport`. REST, MCP, and embedded additionally require `schema`. +""" + + +class ServiceResponseSchema5(ServiceBase): + """ + Service binding in API responses. Includes per-resource transport configuration via typed config. + """ + + +class ServiceResponseSchema6(ServiceResponseSchema1, ServiceResponseSchema5): + """ + Service binding in API responses. Includes per-resource transport configuration via typed config. + """ + + +class ServiceResponseSchema7(ServiceResponseSchema2, ServiceResponseSchema5): + """ + Service binding in API responses. Includes per-resource transport configuration via typed config. + """ + + +class ServiceResponseSchema8(ServiceResponseSchema3, ServiceResponseSchema5): + """ + Service binding in API responses. Includes per-resource transport configuration via typed config. + """ + + +class ServiceResponseSchema9(ServiceResponseSchema4, ServiceResponseSchema5): + """ + Service binding in API responses. Includes per-resource transport configuration via typed config. + """ + + +ServiceResponseSchema = TypeAliasType( + "ServiceResponseSchema", + Annotated[ + ServiceResponseSchema6 + | ServiceResponseSchema7 + | ServiceResponseSchema8 + | ServiceResponseSchema9, + Field(..., title="Service (Response Schema)"), + ], +) +""" +Service binding in API responses. Includes per-resource transport configuration via typed config. +""" + + +class Ucp(BaseModel): + """ + Protocol metadata for discovery profiles and responses. Uses slim schema pattern with context-specific required fields. + """ + + version: Version + map_order: MapOrder | None = None + """ + Preferred key-traversal order for sibling registry fields inside the root `ucp` envelope (`services`, `capabilities`, and `payment_handlers`). + """ + status: Literal["success", "error"] | None = "success" + """ + Application-level status of the UCP operation. + """ + services: dict[ReverseDomainName, list[ServiceBase]] | None = None + """ + Service registry keyed by reverse-domain name. + """ + capabilities: dict[ReverseDomainName, list[CapabilityBase]] | None = None + """ + Capability registry keyed by reverse-domain name. + """ + payment_handlers: ( + dict[ReverseDomainName, list[PaymentHandlerBase]] | None + ) = None + """ + Payment handler registry keyed by reverse-domain name. + """ + + +class UcpBase(BaseModel): + """ + Base UCP metadata with shared properties for all schema types. + """ + + version: Version + map_order: MapOrder | None = None + """ + Preferred key-traversal order for sibling registry fields inside the root `ucp` envelope (`services`, `capabilities`, and `payment_handlers`). + """ + status: Literal["success", "error"] | None = "success" + """ + Application-level status of the UCP operation. + """ + services: dict[ReverseDomainName, list[ServiceBase]] | None = None + """ + Service registry keyed by reverse-domain name. + """ + capabilities: dict[ReverseDomainName, list[CapabilityBase]] | None = None + """ + Capability registry keyed by reverse-domain name. + """ + payment_handlers: ( + dict[ReverseDomainName, list[PaymentHandlerBase]] | None + ) = None + """ + Payment handler registry keyed by reverse-domain name. + """ + + +class UcpBusinessSchema(UcpBase): + """ + UCP metadata for business/merchant-level configuration. Subset of platform schema with business-specific settings. + """ + + supported_versions: dict[Version, AnyUrl] | None = None + """ + Previous protocol versions this business supports, mapped to profile URIs. Businesses that support older protocol versions SHOULD advertise each version and link to its profile. Each URI points to a complete, self-contained profile for that version. When omitted, only `version` is supported. + """ + services: dict[ReverseDomainName, list[ServiceBusinessSchema]] + """ + Service registry keyed by reverse-domain name. + """ + capabilities: ( + dict[ReverseDomainName, list[CapabilityBusinessSchema]] | None + ) = None + """ + Capability registry keyed by reverse-domain name. + """ + payment_handlers: dict[ + ReverseDomainName, list[PaymentHandlerBusinessSchema] + ] + """ + Payment handler registry keyed by reverse-domain name. + """ + + +class UcpCreateRequest(BaseModel): + """ + Request payload to create a new Ucp. Protocol metadata for discovery profiles and responses. Uses slim schema pattern with context-specific required fields. + """ + + version: Version + status: Literal["success", "error"] | None = "success" + """ + Application-level status of the UCP operation. + """ + services: dict[ReverseDomainName, list[ServiceBase]] | None = None + """ + Service registry keyed by reverse-domain name. + """ + capabilities: dict[ReverseDomainName, list[CapabilityBase]] | None = None + """ + Capability registry keyed by reverse-domain name. + """ + payment_handlers: ( + dict[ReverseDomainName, list[PaymentHandlerBase]] | None + ) = None + """ + Payment handler registry keyed by reverse-domain name. + """ + + +class UcpPlatformSchema(UcpBase): + """ + Full UCP metadata for platform-level configuration. Hosted at a URI advertised by the platform in request headers. + """ + + services: dict[ReverseDomainName, list[ServicePlatformSchema]] + """ + Service registry keyed by reverse-domain name. + """ + capabilities: ( + dict[ReverseDomainName, list[CapabilityPlatformSchema]] | None + ) = None + """ + Capability registry keyed by reverse-domain name. + """ + payment_handlers: dict[ + ReverseDomainName, list[PaymentHandlerPlatformSchema] + ] + """ + Payment handler registry keyed by reverse-domain name. + """ + + +class UcpUpdateRequest(BaseModel): + """ + Request payload to update an existing Ucp. Protocol metadata for discovery profiles and responses. Uses slim schema pattern with context-specific required fields. + """ + + version: Version + status: Literal["success", "error"] | None = "success" + """ + Application-level status of the UCP operation. + """ + services: dict[ReverseDomainName, list[ServiceBase]] | None = None + """ + Service registry keyed by reverse-domain name. + """ + capabilities: dict[ReverseDomainName, list[CapabilityBase]] | None = None + """ + Capability registry keyed by reverse-domain name. + """ + payment_handlers: ( + dict[ReverseDomainName, list[PaymentHandlerBase]] | None + ) = None + """ + Payment handler registry keyed by reverse-domain name. + """ + + +class CartCreateRequest(BaseModel): + """ + Request payload to create a new Cart. Shopping cart with estimated pricing before checkout. Lightweight pre-purchase exploration with no payment info or complex status states. + """ + + model_config = ConfigDict( + extra="allow", + ) + line_items: list[LineItemCreateRequest] + """ + Cart line items. Same structure as checkout. Full replacement on update. + """ + context: Context | None = None + """ + Buyer signals for localization (country, region, postal_code). Merchant uses for pricing, availability, currency. Falls back to geo-IP if omitted. + """ + signals: Signals | None = None + attribution: Attribution | None = None + buyer: Buyer | None = None + """ + Buyer with consent tracking. + """ + discounts: DiscountsObject | None = None + + +class CartUpdateRequest(BaseModel): + """ + Request payload to update an existing Cart. Shopping cart with estimated pricing before checkout. Lightweight pre-purchase exploration with no payment info or complex status states. + """ + + model_config = ConfigDict( + extra="allow", + ) + line_items: list[LineItemUpdateRequest] + """ + Cart line items. Same structure as checkout. Full replacement on update. + """ + context: Context | None = None + """ + Buyer signals for localization (country, region, postal_code). Merchant uses for pricing, availability, currency. Falls back to geo-IP if omitted. + """ + signals: Signals | None = None + attribution: Attribution | None = None + buyer: Buyer | None = None + """ + Buyer with consent tracking. + """ + discounts: DiscountsObject | None = None + + +class CheckoutCompleteRequest(BaseModel): + """ + Request payload to complete a Checkout. Base checkout schema. Extensions compose onto this using allOf. + """ + + model_config = ConfigDict( + extra="allow", + ) + buyer: Buyer | None = None + """ + Buyer with consent tracking. + """ + signals: Signals | None = None + attribution: Attribution | None = None + payment: Payment + + +class CheckoutCreateRequest(BaseModel): + """ + Request payload to create a new Checkout. Base checkout schema. Extensions compose onto this using allOf. + """ + + model_config = ConfigDict( + extra="allow", + ) + line_items: list[LineItemCreateRequest] + """ + List of line items being checked out. + """ + buyer: Buyer | None = None + """ + Buyer with consent tracking. + """ + context: Context | None = None + signals: Signals | None = None + attribution: Attribution | None = None + payment: Payment | None = None + discounts: DiscountsObject | None = None + fulfillment: FulfillmentCreateRequest | None = None + """ + Fulfillment details. + """ + + +class CheckoutUpdateRequest(BaseModel): + """ + Request payload to update an existing Checkout. Base checkout schema. Extensions compose onto this using allOf. + """ + + model_config = ConfigDict( + extra="allow", + ) + line_items: list[LineItemUpdateRequest] + """ + List of line items being checked out. + """ + buyer: Buyer | None = None + """ + Buyer with consent tracking. + """ + context: Context | None = None + signals: Signals | None = None + attribution: Attribution | None = None + payment: Payment | None = None + discounts: DiscountsObject | None = None + fulfillment: FulfillmentUpdateRequest | None = None + """ + Fulfillment details. + """ + + +Consent = TypeAliasType("Consent", dict[ReverseDomainName, ConsentPurpose]) +""" +Per-purpose consent. Keys are reverse-DNS purpose identifiers. UCP defines four well-known purposes: `dev.ucp.consent.marketing`, `dev.ucp.consent.analytics`, `dev.ucp.consent.preferences`, `dev.ucp.consent.sale_or_sharing`. Vendors and merchants may define additional purposes under their own reverse-DNS namespace. +""" + + +class DetailProduct(Product): + """ + A product in a get_product response, extended with effective selections and availability signals on option values. + """ + + selected: list[SelectedOption] | None = None + """ + Effective option selections that anchor the featured variant and availability signals. Required when the product has configurable options; may be empty or omitted for products with no option axes. + """ + options: list[Option] | None = None + """ + Product options with availability signals relative to the effective selections. + """ + + +class Error(UcpBase): + """ + UCP metadata with status 'error'. Use for response branches that carry error information. + """ + + status: Literal["error"] = "error" + """ + Application-level status of the UCP operation. + """ + + +class ErrorResponse(BaseModel): + """ + Generic error response when business logic prevents resource creation or failed to retrieve resource. Used when no valid resource can be established. + """ + + model_config = ConfigDict( + extra="forbid", + ) + ucp: Error + """ + UCP protocol metadata. Status MUST be 'error' for error response. + """ + messages: list[Message] = Field(..., min_length=1) + """ + Array of messages describing why the operation failed. + """ + continue_url: AnyUrl | None = None + """ + URL for buyer handoff or session recovery. + """ + + +FulfillmentDestination = TypeAliasType( + "FulfillmentDestination", + Annotated[ + LocationDestination | ShippingDestination, + Field(..., discriminator="type", title="Fulfillment Destination"), + ], +) +""" +A destination for fulfillment. +""" + + +class FulfillmentDetailProduct(DetailProduct): + """ + A get_product detail product (carrying selected/options availability signals) whose variants are fulfillment-enriched. Used by get_product. + """ + + variants: list[FulfillmentVariant] | None = None + + +class FulfillmentLookupVariant(LookupVariant): + """ + A lookup variant (carrying input correlation) enriched with fulfillment. + """ + + fulfillment: CatalogFulfillment | None = None + + +class FulfillmentMethod(BaseModel): + """ + A fulfillment method with destinations and groups. + """ + + id: str + """ + Unique fulfillment method identifier. + """ + type: str + """ + Fulfillment method type. Well-known values: `shipping`, `pickup`. Businesses MAY use additional values. + """ + line_item_ids: list[str] + """ + Line item IDs fulfilled via this method. + """ + destinations: list[FulfillmentDestination] | None = None + """ + Available destinations for this method. In Business responses, each destination carries a `type` and `id`. + """ + selected_destination_id: str | None = None + """ + ID of the selected destination. Accepts any stable, Business-scoped ID the Business recognizes for this method, including Location IDs not yet enumerated in `destinations`. + """ + groups: list[FulfillmentGroup] | None = None + """ + Fulfillment groups for selecting options. Agent sets selected_option_id on groups to choose shipping method. + """ + + +class FulfillmentProduct(Product): + """ + A catalog product whose variants are fulfillment-enriched. Used by search. + """ + + variants: list[FulfillmentVariant] | None = Field(None, min_length=1) + """ + Purchasable variants of this product. First item is the featured variant for listings. + """ + + +class Product1(Product): + variants: list[LookupVariant] | None = Field(None, min_length=1) + """ + Purchasable variants of this product. First item is the featured variant for listings. + """ + + +class ProfileBase(BaseModel): + """ + Common wrapper for UCP profile documents. + """ + + model_config = ConfigDict( + extra="allow", + ) + ucp: UcpBase + """ + Protocol metadata, capabilities, services, and payment handlers advertised by this party. + """ + keys: list[JwkPublicKey] | None = None + """ + Canonical UCP profile field for publishing signing keys, as a JWK Set per RFC 7517. When a profile publishes signing keys, they MUST appear here; this is where every UCP verifier reads them. Publishing keys[] makes the UCP profile a valid JWK Set that a signer can reuse as its Web Bot Auth key source: a WBA-shape verifier resolving via Signature-Agent type=jwks_uri pointed at this profile reads these keys, and the cimd and directory variants reach them through their own documents. See the Deployment Patterns for WBA Interop section in the overview for hosting patterns. + """ + + +class ProfileBusinessSchema(ProfileBase): + """ + Profile document hosted by a business at /.well-known/ucp. + """ + + ucp: UcpBusinessSchema | None = None + """ + Protocol metadata, capabilities, services, and payment handlers advertised by this party. + """ + + +class ProfilePlatformSchema(ProfileBase): + """ + Profile document hosted by a platform and advertised to businesses via UCP-Agent. + """ + + ucp: UcpPlatformSchema | None = None + """ + Protocol metadata, capabilities, services, and payment handlers advertised by this party. + """ + + +class ResponseCartSchema(UcpBase): + """ + UCP metadata for cart responses. No payment handlers needed pre-checkout. + """ + + capabilities: ( + dict[ReverseDomainName, list[CapabilityResponseSchema]] | None + ) = None + """ + Capability registry keyed by reverse-domain name. + """ + + +class ResponseCatalogSchema(UcpBase): + """ + UCP metadata for catalog responses. + """ + + capabilities: ( + dict[ReverseDomainName, list[CapabilityResponseSchema]] | None + ) = None + """ + Capability registry keyed by reverse-domain name. + """ + + +class ResponseCheckoutSchema(UcpBase): + """ + UCP metadata for checkout responses. + """ + + services: dict[ReverseDomainName, list[ServiceResponseSchema]] | None = None + """ + Service registry keyed by reverse-domain name. + """ + capabilities: ( + dict[ReverseDomainName, list[CapabilityResponseSchema]] | None + ) = None + """ + Capability registry keyed by reverse-domain name. + """ + payment_handlers: dict[ + ReverseDomainName, list[PaymentHandlerResponseSchema] + ] + """ + Payment handler registry keyed by reverse-domain name. + """ + + +class ResponseLocationSchema(UcpBase): + """ + UCP metadata for location responses. + """ + + capabilities: ( + dict[ReverseDomainName, list[CapabilityResponseSchema]] | None + ) = None + """ + Capability registry keyed by reverse-domain name. + """ + + +class ResponseOrderSchema(UcpBase): + """ + UCP metadata for order responses. No payment handlers needed post-purchase. + """ + + capabilities: ( + dict[ReverseDomainName, list[CapabilityResponseSchema]] | None + ) = None + """ + Capability registry keyed by reverse-domain name. + """ + + +class SearchResponse(BaseModel): + ucp: ResponseCatalogSchema + products: list[Product] + """ + Products matching the search criteria. + """ + pagination: Response | None = None + actions: Actions | None = None + """ + Outstanding extension-defined Actions for this catalog search response. + """ + messages: list[Message] | None = None + """ + Errors, warnings, or informational messages about the search results. + """ + policies: list[Policy] | None = None + """ + Policies (e.g., return/refund terms) that apply to the products in these search results. `applies_to` targets are relative to the response root; when absent or empty, refer to the URLs in `links[]`. + """ + + +class Success(UcpBase): + """ + UCP metadata with status 'success'. Use for response branches that carry the expected payload. + """ + + status: Literal["success"] = "success" + """ + Application-level status of the UCP operation. + """ + + +class Cart(BaseModel): + """ + Shopping cart with estimated pricing before checkout. Lightweight pre-purchase exploration with no payment info or complex status states. + """ + + model_config = ConfigDict( + extra="allow", + ) + ucp: ResponseCartSchema + id: str + """ + Unique cart identifier. + """ + line_items: list[LineItemModel] + """ + Cart line items. Same structure as checkout. Full replacement on update. + """ + context: Context | None = None + """ + Buyer signals for localization (country, region, postal_code). Merchant uses for pricing, availability, currency. Falls back to geo-IP if omitted. + """ + signals: Signals | None = None + attribution: Attribution | None = None + buyer: Buyer | None = None + """ + Buyer with consent tracking. + """ + currency: str + """ + ISO 4217 currency code. Determined by merchant based on context or geo-IP. + """ + totals: Totals + """ + Estimated cost breakdown. May be partial if shipping/tax not yet calculable. + """ + actions: Actions | None = None + """ + Outstanding extension-defined Actions for this cart. + """ + messages: list[Message] | None = None + """ + Validation messages, warnings, or informational notices. + """ + links: list[Link] | None = None + """ + Optional merchant links (policies, FAQs). + """ + policies: list[Policy] | None = None + """ + Policies (e.g., return/refund terms) that apply to the items in this cart. `applies_to` targets are relative to the response root; when absent or empty, refer to the URLs in `links[]`. + """ + continue_url: AnyUrl | None = None + """ + URL for cart handoff and session recovery. Enables sharing and human-in-the-loop flows. + """ + expires_at: AwareDatetime | None = None + """ + Cart expiry timestamp (RFC 3339). Optional. + """ + discounts: DiscountsObject | None = None + + +class Fulfillment(BaseModel): + """ + Container for fulfillment methods and availability. + """ + + methods: list[FulfillmentMethod] | None = None + """ + Fulfillment methods for cart items. + """ + available_methods: list[FulfillmentAvailableMethod] | None = None + """ + Inventory availability hints. + """ + + +class FulfillmentLookupProduct(Product): + """ + A lookup product whose variants are fulfillment-enriched, preserving input correlation. Used by lookup. + """ + + variants: list[FulfillmentLookupVariant] | None = Field(None, min_length=1) + """ + Purchasable variants of this product. First item is the featured variant for listings. + """ + + +class FulfillmentSearchResponse(SearchResponse): + products: list[FulfillmentProduct] | None = None + """ + Products matching the search criteria. + """ + + +class GetProductResponse(BaseModel): + ucp: ResponseCatalogSchema + product: DetailProduct + """ + The requested product with full detail. Singular — this is a single-resource operation. + """ + actions: Actions | None = None + """ + Outstanding extension-defined Actions for this product response. + """ + messages: list[Message] | None = None + """ + Warnings or informational messages about the product (e.g., price recently changed, limited availability). + """ + policies: list[Policy] | None = None + """ + Policies (e.g., return/refund terms) that apply to this product. `applies_to` targets are relative to the response root; when absent or empty, refer to the URLs in `links[]`. + """ + + +class LookupResponse(BaseModel): + ucp: ResponseCatalogSchema + products: list[Product1] + """ + Products matching the requested identifiers. May contain fewer items if some identifiers not found, or more if identifiers match multiple products. + """ + actions: Actions | None = None + """ + Outstanding extension-defined Actions for this catalog lookup response. + """ + messages: list[Message] | None = None + """ + Errors, warnings, or informational messages about the requested items. + """ + policies: list[Policy] | None = None + """ + Policies (e.g., return/refund terms) that apply to the products in this response. `applies_to` targets are relative to the response root; when absent or empty, refer to the URLs in `links[]`. + """ + + +class Order(BaseModel): + """ + Order schema with line items, buyer-facing fulfillment expectations, and event logs. + """ + + ucp: ResponseOrderSchema + id: str + """ + Unique order identifier. + """ + label: str | None = None + """ + Human-readable label for identifying the order. MUST only be provided by the business. + """ + checkout_id: str + """ + Associated checkout ID for reconciliation. + """ + permalink_url: AnyUrl + """ + Permalink to access the order on merchant site. + """ + line_items: list[OrderLineItem] + """ + Line items representing what was purchased — can change post-order via edits or exchanges. + """ + fulfillment: Fulfillment1 + """ + Fulfillment data: buyer expectations and what actually happened. + """ + adjustments: list[Adjustment] | None = None + """ + Post-order events (refunds, returns, credits, disputes, cancellations, etc.) that exist independently of fulfillment. + """ + currency: str + """ + ISO 4217 currency code. MUST match the currency from the originating checkout session. + """ + totals: Totals + """ + Different totals for the order. + """ + policies: list[Policy] | None = None + """ + Snapshot of the policies that applied to the items at checkout, captured on the order as a durable record. `applies_to` targets are relative to the response root. + """ + messages: list[Message] | None = None + """ + Business outcome messages (errors, warnings, informational). Present when the business needs to communicate status or issues to the platform. + """ + attribution: Attribution | None = None + """ + Snapshot of the attribution associated with the originating checkout. Read-only on the order. + """ + + +class OrderCreateRequest(BaseModel): + """ + Request payload to create a new Order. Order schema with line items, buyer-facing fulfillment expectations, and event logs. + """ + + ucp: ResponseOrderSchema + id: str + """ + Unique order identifier. + """ + label: str | None = None + """ + Human-readable label for identifying the order. MUST only be provided by the business. + """ + checkout_id: str + """ + Associated checkout ID for reconciliation. + """ + permalink_url: AnyUrl + """ + Permalink to access the order on merchant site. + """ + line_items: list[OrderLineItem] + """ + Line items representing what was purchased — can change post-order via edits or exchanges. + """ + fulfillment: Fulfillment1 + """ + Fulfillment data: buyer expectations and what actually happened. + """ + adjustments: list[Adjustment] | None = None + """ + Post-order events (refunds, returns, credits, disputes, cancellations, etc.) that exist independently of fulfillment. + """ + totals: TotalsCreateRequest + """ + Different totals for the order. + """ + messages: list[Message] | None = None + """ + Business outcome messages (errors, warnings, informational). Present when the business needs to communicate status or issues to the platform. + """ + + +class OrderUpdateRequest(BaseModel): + """ + Request payload to update an existing Order. Order schema with line items, buyer-facing fulfillment expectations, and event logs. + """ + + ucp: ResponseOrderSchema + id: str + """ + Unique order identifier. + """ + label: str | None = None + """ + Human-readable label for identifying the order. MUST only be provided by the business. + """ + checkout_id: str + """ + Associated checkout ID for reconciliation. + """ + permalink_url: AnyUrl + """ + Permalink to access the order on merchant site. + """ + line_items: list[OrderLineItem] + """ + Line items representing what was purchased — can change post-order via edits or exchanges. + """ + fulfillment: Fulfillment1 + """ + Fulfillment data: buyer expectations and what actually happened. + """ + adjustments: list[Adjustment] | None = None + """ + Post-order events (refunds, returns, credits, disputes, cancellations, etc.) that exist independently of fulfillment. + """ + totals: TotalsUpdateRequest + """ + Different totals for the order. + """ + messages: list[Message] | None = None + """ + Business outcome messages (errors, warnings, informational). Present when the business needs to communicate status or issues to the platform. + """ + + +class Profile(ProfileBase): + """ + Variant-neutral wrapper schema for UCP profile documents. Use the business_schema definition to validate business profiles and the platform_schema definition to validate platform profiles. + """ + + +class Checkout(BaseModel): + """ + Base checkout schema. Extensions compose onto this using allOf. + """ + + model_config = ConfigDict( + extra="allow", + ) + ucp: ResponseCheckoutSchema + id: str + """ + Unique identifier of the checkout session. + """ + line_items: list[LineItemModel] + """ + List of line items being checked out. + """ + buyer: Buyer | None = None + """ + Buyer with consent tracking. + """ + context: Context | None = None + signals: Signals | None = None + attribution: Attribution | None = None + status: Literal[ + "incomplete", + "requires_escalation", + "ready_for_complete", + "complete_in_progress", + "completed", + "canceled", + ] + """ + Checkout state indicating the current phase and required processing. See Checkout Status lifecycle documentation for state transition details. + """ + currency: str + """ + ISO 4217 currency code reflecting the merchant's market determination. Derived from address, context, and geo IP—buyers provide signals, merchants determine currency. + """ + totals: Totals + """ + Different cart totals. + """ + actions: Actions | None = None + """ + Outstanding extension-defined Actions for this checkout. + """ + messages: list[Message] | None = None + """ + List of messages with error and info about the checkout session state. + """ + links: list[Link] + """ + Links to be displayed by the platform (Privacy Policy, TOS). Mandatory for legal compliance. + """ + policies: list[Policy] | None = None + """ + Policies (e.g., return/refund terms) that apply to the items in this checkout. `applies_to` targets are relative to the response root; when absent or empty, refer to the URLs in `links[]`. + """ + expires_at: AwareDatetime | None = None + """ + RFC 3339 expiry timestamp. Default TTL is 6 hours from creation if not sent. + """ + continue_url: AnyUrl | None = None + """ + URL for checkout handoff and session recovery. MUST be provided when status is requires_escalation. See specification for format and availability requirements. + """ + payment: Payment | None = None + order: OrderConfirmation | None = None + """ + Details about an order created for this checkout session. + """ + discounts: DiscountsObject | None = None + fulfillment: Fulfillment | None = None + """ + Fulfillment details. + """ + + +class FulfillmentGetProductResponse(GetProductResponse): + product: FulfillmentDetailProduct | None = None + """ + The requested product with full detail. Singular — this is a single-resource operation. + """ + + +class FulfillmentLookupResponse(LookupResponse): + products: list[FulfillmentLookupProduct] | None = None + """ + Products matching the requested identifiers. May contain fewer items if some identifiers not found, or more if identifiers match multiple products. + """ + + +ConstraintExpression.model_rebuild() diff --git a/tests/test_codegen_pipeline.py b/tests/test_codegen_pipeline.py index f03252a..75a65fd 100644 --- a/tests/test_codegen_pipeline.py +++ b/tests/test_codegen_pipeline.py @@ -19,14 +19,11 @@ import copy import io import json -import sys import tempfile import unittest from pathlib import Path -from unittest import mock import postprocess_models -import preprocess_schemas try: from pydantic import TypeAdapter, ValidationError @@ -59,977 +56,6 @@ HAVE_SDK = False -class SchemaNormalizationTest(unittest.TestCase): - """Tests schema flattening and reference normalization.""" - - def test_iter_nodes_expands_properties_without_yielding_container( - self, - ) -> None: - """iter_nodes yields "properties" map values directly rather than the "properties" map as a container. Avoids traversal of property names that match json schema keywords.""" - schema = { - "type": "object", - "properties": { - "user": {"type": "string", "$ref": "user.json"}, - "allOf": {"type": "object"}, - }, - } - nodes = list(preprocess_schemas.iter_nodes(schema)) - - # The root object is yielded - self.assertIn(schema, nodes) - # The property subschemas are yielded - self.assertIn(schema["properties"]["user"], nodes) - self.assertIn(schema["properties"]["allOf"], nodes) - # The container map {"user": ..., "allOf": ...} itself is NOT yielded - self.assertNotIn(schema["properties"], nodes) - - def test_resolve_local_ref_supports_objects_and_arrays(self) -> None: - """Local JSON pointers resolve object keys and array indexes.""" - schema = {"$defs": {"choices": [{"const": "first"}]}} - - resolved = preprocess_schemas.resolve_local_ref( - "#/$defs/choices/0", schema - ) - - self.assertEqual(resolved, {"const": "first"}) - self.assertIsNone( - preprocess_schemas.resolve_local_ref("#/$defs/choices/1", schema) - ) - self.assertIsNone( - preprocess_schemas.resolve_local_ref("other.json", schema) - ) - - def test_resolve_local_refs_inlines_nested_and_transitive_pointers( - self, - ) -> None: - """Local references in fragment are inlined recursively with overrides.""" - root = { - "$defs": { - "base_version": { - "type": "string", - "pattern": r"^\d{4}-\d{2}-\d{2}$", - "description": "Default version description", - }, - "version_alias": { - "$ref": "#/$defs/base_version", - }, - } - } - fragment = { - "type": "object", - "properties": { - "version": { - "$ref": "#/$defs/version_alias", - "description": "Entity version in YYYY-MM-DD format.", - } - }, - } - - preprocess_schemas.resolve_local_refs(fragment, root) - - self.assertEqual( - fragment["properties"]["version"], - { - "type": "string", - "pattern": r"^\d{4}-\d{2}-\d{2}$", - "description": "Entity version in YYYY-MM-DD format.", - }, - ) - - def test_main_inlines_entity_local_refs_without_dangling_pointers( - self, - ) -> None: - """Entity local refs like $defs/version are resolved before inlining into child schemas.""" - with tempfile.TemporaryDirectory() as temp_dir: - root = Path(temp_dir) - preprocess_schemas.save_json( - { - "$defs": { - "version": { - "type": "string", - "pattern": r"^\d{4}-\d{2}-\d{2}$", - }, - "entity": { - "type": "object", - "properties": { - "version": {"$ref": "#/$defs/version"}, - "id": {"type": "string"}, - }, - "required": ["version"], - }, - } - }, - root / "ucp.json", - ) - preprocess_schemas.save_json( - { - "$id": "https://ucp.dev/schemas/capability.json", - "title": "Capability", - "$defs": { - "base": { - "allOf": [{"$ref": "ucp.json#/$defs/entity"}], - } - }, - }, - root / "capability.json", - ) - - with ( - mock.patch.object( - sys, - "argv", - ["preprocess_schemas.py", str(root)], - ), - contextlib.redirect_stdout(io.StringIO()), - ): - preprocess_schemas.main() - - capability = preprocess_schemas.load_json(root / "capability.json") - base = capability["$defs"]["base"] - self.assertNotIn("allOf", base) - self.assertEqual( - base["properties"]["version"], - { - "type": "string", - "pattern": r"^\d{4}-\d{2}-\d{2}$", - }, - ) - self.assertNotIn("$ref", base["properties"]["version"]) - - def test_preprocess_flattens_and_distributes_properties(self) -> None: - """Flattened base fields are distributed to polymorphic branches.""" - schema = { - "$defs": { - "base": { - "type": "object", - "properties": {"id": {"type": "string"}}, - "required": ["id"], - } - }, - "allOf": [{"$ref": "#/$defs/base"}], - "oneOf": [ - { - "properties": {"kind": {"const": "physical"}}, - "required": ["kind"], - } - ], - } - - preprocess_schemas.preprocess_full_schema(schema) - - self.assertNotIn("allOf", schema) - self.assertEqual(schema["required"], ["id"]) - branch = schema["oneOf"][0] - self.assertEqual(set(branch["properties"]), {"id", "kind"}) - self.assertEqual(set(branch["required"]), {"id", "kind"}) - self.assertEqual(branch["type"], "object") - - def test_preprocess_preserves_multiple_conditional_branches(self) -> None: - """Each conditional allOf branch survives schema flattening.""" - negative = { - "if": {"properties": {"type": {"const": "discount"}}}, - "then": {"properties": {"amount": {"exclusiveMaximum": 0}}}, - } - non_negative = { - "if": {"properties": {"type": {"const": "subtotal"}}}, - "then": {"properties": {"amount": {"minimum": 0}}}, - } - schema = { - "type": "object", - "properties": { - "type": {"type": "string"}, - "amount": {"type": "integer"}, - }, - "allOf": [negative, non_negative], - } - - preprocess_schemas.preprocess_full_schema(schema) - - self.assertEqual(schema["allOf"], [negative, non_negative]) - - def test_preprocess_inlines_entity_fields(self) -> None: - """The shared entity definition is inlined without its metadata.""" - entity = { - "title": "Entity", - "description": "Shared entity fields.", - "type": "object", - "properties": {"id": {"type": "string"}}, - "required": ["id"], - } - schema = { - "allOf": [ - {"$ref": "ucp.json#/$defs/entity"}, - { - "type": "object", - "properties": {"value": {"type": "integer"}}, - "required": ["value"], - }, - ] - } - - preprocess_schemas.preprocess_full_schema(schema, entity) - - self.assertEqual(set(schema["properties"]), {"id", "value"}) - self.assertEqual(set(schema["required"]), {"id", "value"}) - self.assertNotIn("title", schema) - self.assertNotIn("description", schema) - - def test_flatten_dotted_defs_rewrites_local_refs(self) -> None: - """Dotted definition names and local references stay aligned.""" - schema = { - "$defs": { - "checkout": {"type": "string"}, - "dev.ucp.shopping.checkout": {"type": "object"}, - }, - "properties": { - "checkout": {"$ref": "#/$defs/dev.ucp.shopping.checkout"} - }, - } - - rename_map = preprocess_schemas.flatten_dotted_defs(schema) - - self.assertEqual( - rename_map, - {"dev.ucp.shopping.checkout": "dev_ucp_shopping_checkout"}, - ) - self.assertIn("dev_ucp_shopping_checkout", schema["$defs"]) - self.assertEqual( - schema["properties"]["checkout"]["$ref"], - "#/$defs/dev_ucp_shopping_checkout", - ) - - def test_flatten_dotted_defs_splits_capability_role_containers( - self, - ) -> None: - """Role containers split into two defs and refs into them follow.""" - role_platform = {"title": "Platform", "allOf": [{"type": "object"}]} - role_business = {"title": "Business", "allOf": [{"type": "object"}]} - schema = { - "$defs": { - "dev.ucp.common.identity_linking": { - "platform_schema": role_platform, - "business_schema": role_business, - }, - }, - "properties": { - "platform": { - "$ref": "#/$defs/dev.ucp.common.identity_linking/platform_schema" - }, - "business": { - "$ref": "#/$defs/dev.ucp.common.identity_linking/business_schema" - }, - }, - } - - rename_map = preprocess_schemas.flatten_dotted_defs(schema) - - self.assertEqual( - rename_map, - { - "dev.ucp.common.identity_linking/platform_schema": ( - "identity_linking_platform_schema" - ), - "dev.ucp.common.identity_linking/business_schema": ( - "identity_linking_business_schema" - ), - }, - ) - self.assertEqual( - schema["$defs"], - { - "identity_linking_platform_schema": role_platform, - "identity_linking_business_schema": role_business, - }, - ) - self.assertEqual( - schema["properties"]["platform"]["$ref"], - "#/$defs/identity_linking_platform_schema", - ) - self.assertEqual( - schema["properties"]["business"]["$ref"], - "#/$defs/identity_linking_business_schema", - ) - - def test_rewrite_external_defs_refs_uses_target_rename_map(self) -> None: - """External references follow renames made in the target schema.""" - with tempfile.TemporaryDirectory() as temp_dir: - root = Path(temp_dir) - source_path = root / "source.json" - target_path = root / "target.json" - schema = { - "properties": { - "checkout": { - "$ref": ("target.json#/$defs/dev.ucp.shopping.checkout") - } - } - } - - preprocess_schemas._rewrite_external_defs_refs( - source_path, - schema, - { - str(target_path.resolve()): { - "dev.ucp.shopping.checkout": "checkout" - } - }, - ) - - self.assertEqual( - schema["properties"]["checkout"]["$ref"], - "target.json#/$defs/checkout", - ) - - -class RequestMetadataTest(unittest.TestCase): - """Tests operation-specific request metadata rules.""" - - def test_get_required_ops_collects_all_declared_operations(self) -> None: - """String and mapping markers contribute their operations.""" - schema = { - "properties": { - "id": {"ucp_request": "omit"}, - "payment": { - "ucp_request": { - "complete": "required", - "update": "optional", - } - }, - "plain": {"type": "string"}, - } - } - - self.assertEqual( - preprocess_schemas.get_required_ops(schema), - {"create", "update", "complete"}, - ) - - def test_eval_prop_inclusion_applies_operation_overrides(self) -> None: - """Operation markers override base required and inclusion rules.""" - cases = [ - ("default-required", {}, "create", ["field"], (True, True)), - ( - "simple-optional", - {"ucp_request": "optional"}, - "create", - ["field"], - (True, False), - ), - ( - "simple-omit", - {"ucp_request": "omit"}, - "create", - [], - (False, False), - ), - ( - "operation-required", - {"ucp_request": {"create": "required"}}, - "create", - [], - (True, True), - ), - ( - "operation-omit", - {"ucp_request": {"create": "omit"}}, - "create", - ["field"], - (False, True), - ), - ( - "transition-omit", - { - "ucp_request": { - "update": { - "transition": {"from": "required", "to": "omit"} - } - } - }, - "update", - ["field"], - (False, True), - ), - ( - "undeclared-operation", - {"ucp_request": {"update": "required"}}, - "create", - [], - (False, False), - ), - ] - - for name, data, operation, required, expected in cases: - with self.subTest(name=name): - actual = preprocess_schemas.eval_prop_inclusion( - "field", data, operation, required - ) - self.assertEqual(actual, expected) - - -class VariantGenerationTest(unittest.TestCase): - """Tests request variant construction and output.""" - - def test_rewrite_external_ref_preserves_fragment(self) -> None: - """External refs target variants without losing their fragments.""" - schema = { - "properties": { - "child": {"$ref": "nested/child.json#/$defs/item"}, - "local": {"$ref": "#/$defs/local"}, - } - } - file_path = Path("/schemas/parent.json") - child_path = str((file_path.parent / "nested" / "child.json").resolve()) - - preprocess_schemas.rewrite_refs_to_variants( - schema, - "create", - file_path, - {child_path: {"create"}}, - ) - - self.assertEqual( - schema["properties"]["child"]["$ref"], - "nested/child_create_request.json#/$defs/item", - ) - self.assertEqual( - schema["properties"]["local"]["$ref"], - "#/$defs/local", - ) - - def test_object_variant_filters_fields_and_rewrites_refs(self) -> None: - """Object variants filter fields and target child variants.""" - schema = { - "$id": "https://ucp.dev/schemas/checkout.json", - "title": "Checkout", - "type": "object", - "properties": { - "id": { - "type": "string", - "ucp_request": { - "create": "omit", - "update": "required", - }, - }, - "currency": { - "type": "string", - "ucp_request": "required", - }, - "note": { - "type": "string", - "ucp_request": "optional", - }, - "server_only": { - "type": "string", - "ucp_request": "omit", - }, - "child": { - "$ref": "child.json", - "ucp_request": "required", - }, - }, - "required": ["id", "note"], - } - original = copy.deepcopy(schema) - file_path = Path("/schemas/checkout.json") - child_path = str((file_path.parent / "child.json").resolve()) - - variant = preprocess_schemas._create_single_variant( - schema, - "create", - "checkout", - file_path, - {child_path: {"create"}}, - ) - - self.assertEqual(schema, original) - self.assertEqual(variant["title"], "Checkout Create Request") - self.assertEqual( - variant["$id"], - "https://ucp.dev/schemas/checkout_create_request.json", - ) - self.assertEqual( - set(variant["properties"]), {"currency", "note", "child"} - ) - self.assertEqual(set(variant["required"]), {"currency", "child"}) - self.assertEqual( - variant["properties"]["child"]["$ref"], - "child_create_request.json", - ) - for data in variant["properties"].values(): - self.assertNotIn("ucp_request", data) - - def test_array_variant_preserves_root_and_filters_nested_objects( - self, - ) -> None: - """Array roots stay arrays while nested request fields are filtered.""" - schema = { - "$id": "https://ucp.dev/schemas/totals.json", - "title": "Totals", - "type": "array", - "items": { - "allOf": [ - { - "type": "object", - "properties": { - "amount": {"type": "integer"}, - "label": { - "type": "string", - "ucp_request": {"create": "required"}, - }, - "lines": { - "type": "array", - "ucp_request": {"create": "omit"}, - }, - }, - "required": ["amount"], - } - ] - }, - } - - variant = preprocess_schemas._create_single_variant( - schema, - "create", - "totals", - Path("/schemas/totals.json"), - {}, - ) - - self.assertEqual(variant["type"], "array") - self.assertNotIn("properties", variant) - self.assertNotIn("required", variant) - item_schema = variant["items"]["allOf"][0] - self.assertEqual(set(item_schema["properties"]), {"amount", "label"}) - self.assertEqual(set(item_schema["required"]), {"amount", "label"}) - self.assertEqual(variant["title"], "Totals Create Request") - - def test_composition_variant_rewrites_refs(self) -> None: - """Composition variants (oneOf/anyOf/allOf) rewrite refs to variants.""" - schema = { - "$id": "https://ucp.dev/schemas/poly.json", - "title": "Poly", - "oneOf": [{"$ref": "child_a.json"}, {"$ref": "child_b.json"}], - "allOf": [{"$ref": "parent.json"}], - "anyOf": [{"$ref": "other.json"}], - } - file_path = Path("/schemas/poly.json") - child_a_path = str((file_path.parent / "child_a.json").resolve()) - child_b_path = str((file_path.parent / "child_b.json").resolve()) - parent_path = str((file_path.parent / "parent.json").resolve()) - other_path = str((file_path.parent / "other.json").resolve()) - - variant_needs = { - child_a_path: {"create"}, - child_b_path: {"create"}, - parent_path: {"create"}, - other_path: {"create"}, - } - - variant = preprocess_schemas._create_single_variant( - schema, - "create", - "poly", - file_path, - variant_needs, - ) - - self.assertEqual( - variant["oneOf"][0]["$ref"], "child_a_create_request.json" - ) - self.assertEqual( - variant["oneOf"][1]["$ref"], "child_b_create_request.json" - ) - self.assertEqual( - variant["allOf"][0]["$ref"], "parent_create_request.json" - ) - self.assertEqual( - variant["anyOf"][0]["$ref"], "other_create_request.json" - ) - - def test_generate_variants_writes_operation_specific_files(self) -> None: - """Variant generation writes one filtered file per operation.""" - schema = { - "title": "Product", - "type": "object", - "properties": { - "id": { - "type": "string", - "ucp_request": { - "create": "omit", - "update": "required", - }, - } - }, - "required": ["id"], - } - - with tempfile.TemporaryDirectory() as temp_dir: - source_path = Path(temp_dir) / "product.json" - with contextlib.redirect_stdout(io.StringIO()): - preprocess_schemas.generate_variants( - source_path, - schema, - {"create", "update"}, - {}, - ) - - create_variant = preprocess_schemas.load_json( - Path(temp_dir) / "product_create_request.json" - ) - update_variant = preprocess_schemas.load_json( - Path(temp_dir) / "product_update_request.json" - ) - - self.assertEqual(create_variant["properties"], {}) - self.assertEqual(create_variant["required"], []) - self.assertEqual(set(update_variant["properties"]), {"id"}) - self.assertEqual(update_variant["required"], ["id"]) - - -class PipelineDependencyTest(unittest.TestCase): - """Tests metadata normalization and transitive variant dependencies.""" - - def test_normalize_metadata_schemas_sets_root_anyof_and_ucp_refs( - self, - ) -> None: - """Equivalent response profiles remain valid metadata alternatives.""" - target_dir = Path("/schemas") - ucp_path = str((target_dir / "ucp.json").resolve()) - checkout_path = str((target_dir / "checkout.json").resolve()) - request_path = str( - (target_dir / "checkout_create_request.json").resolve() - ) - schemas = { - ucp_path: { - "$defs": { - "version": {"type": "string"}, - "entity": {"type": "object"}, - "platform_schema": {"type": "object"}, - "business_schema": {"type": "object"}, - "response_checkout_schema": {"type": "object"}, - "response_order_schema": {"type": "object"}, - "response_cart_schema": {"type": "object"}, - "response_catalog_schema": {"type": "object"}, - } - }, - checkout_path: { - "properties": { - "ucp": {"$ref": "ucp.json#/$defs/response_schema"} - } - }, - request_path: { - "properties": { - "ucp": {"$ref": "ucp.json#/$defs/request_schema"} - } - }, - } - - preprocess_schemas.normalize_metadata_schemas(schemas, target_dir) - - self.assertNotIn("oneOf", schemas[ucp_path]) - self.assertEqual( - schemas[ucp_path]["anyOf"], - [ - {"$ref": "#/$defs/platform_schema"}, - {"$ref": "#/$defs/business_schema"}, - {"$ref": "#/$defs/response_checkout_schema"}, - {"$ref": "#/$defs/response_order_schema"}, - {"$ref": "#/$defs/response_cart_schema"}, - {"$ref": "#/$defs/response_catalog_schema"}, - ], - ) - self.assertEqual( - schemas[checkout_path]["properties"]["ucp"]["$ref"], - "ucp.json", - ) - self.assertEqual( - schemas[request_path]["properties"]["ucp"]["$ref"], - "ucp.json#/$defs/request_schema", - ) - - def test_variant_needs_propagate_transitively_and_respect_omit( - self, - ) -> None: - """Variant dependencies propagate only through included properties.""" - parent_path = "/schemas/parent.json" - child_path = "/schemas/child.json" - grandchild_path = "/schemas/grandchild.json" - schemas = { - parent_path: { - "properties": { - "child": { - "$ref": "child.json", - "ucp_request": { - "create": "required", - "update": "omit", - }, - } - } - }, - child_path: { - "properties": {"grandchild": {"$ref": "grandchild.json"}} - }, - grandchild_path: {"properties": {}}, - } - schema_refs = { - parent_path: [("child", child_path)], - child_path: [("grandchild", grandchild_path)], - grandchild_path: [], - } - variant_needs = {parent_path: {"create", "update"}} - - preprocess_schemas.propagate_needs_transitive( - variant_needs, schema_refs, schemas - ) - - self.assertEqual(variant_needs[child_path], {"create"}) - self.assertEqual(variant_needs[grandchild_path], {"create"}) - - def test_variant_needs_propagate_through_composition_keywords(self) -> None: - """Variant dependencies propagate unconditionally through oneOf/anyOf/allOf/items.""" - parent_path = "/schemas/parent.json" - child_path = "/schemas/child.json" - schemas = { - parent_path: {"oneOf": [{"$ref": "child.json"}]}, - child_path: {"properties": {}}, - } - schema_refs = { - parent_path: [("oneOf", child_path)], - child_path: [], - } - variant_needs = {parent_path: {"create", "update"}} - - preprocess_schemas.propagate_needs_transitive( - variant_needs, schema_refs, schemas - ) - - self.assertEqual(variant_needs[child_path], {"create", "update"}) - - def test_main_preprocesses_schema_tree_end_to_end(self) -> None: - """The full pipeline normalizes schemas and writes linked variants.""" - with tempfile.TemporaryDirectory() as temp_dir: - root = Path(temp_dir) - preprocess_schemas.save_json( - { - "$defs": { - "entity": { - "type": "object", - "properties": {"id": {"type": "string"}}, - "required": ["id"], - } - } - }, - root / "ucp.json", - ) - preprocess_schemas.save_json( - { - "$id": "https://ucp.dev/schemas/child.json", - "title": "Child", - "type": "object", - "properties": { - "value": { - "type": "string", - "ucp_request": {"create": "required"}, - } - }, - }, - root / "child.json", - ) - preprocess_schemas.save_json( - { - "$id": "https://ucp.dev/schemas/parent.json", - "title": "Parent", - "allOf": [{"$ref": "ucp.json#/$defs/entity"}], - "properties": { - "child": { - "$ref": "child.json", - "ucp_request": {"create": "required"}, - } - }, - }, - root / "parent.json", - ) - - with ( - mock.patch.object( - sys, - "argv", - ["preprocess_schemas.py", str(root)], - ), - contextlib.redirect_stdout(io.StringIO()), - ): - preprocess_schemas.main() - - parent = preprocess_schemas.load_json(root / "parent.json") - parent_variant = preprocess_schemas.load_json( - root / "parent_create_request.json" - ) - child_variant = preprocess_schemas.load_json( - root / "child_create_request.json" - ) - - self.assertNotIn("allOf", parent) - self.assertEqual(set(parent["properties"]), {"id", "child"}) - self.assertEqual( - parent_variant["properties"]["child"]["$ref"], - "child_create_request.json", - ) - self.assertEqual(set(parent_variant["required"]), {"id", "child"}) - self.assertEqual(child_variant["required"], ["value"]) - - def test_propagation_with_fragment(self) -> None: - """Propagation should work even if the reference has a fragment.""" - with tempfile.TemporaryDirectory() as temp_dir: - root = Path(temp_dir) - preprocess_schemas.save_json( - { - "$defs": { - "entity": { - "type": "object", - "properties": {"id": {"type": "string"}}, - "required": ["id"], - } - } - }, - root / "ucp.json", - ) - preprocess_schemas.save_json( - { - "$id": "https://ucp.dev/schemas/child.json", - "title": "Child", - "type": "object", - "$defs": { - "item": { - "type": "object", - "properties": { - "grandchild": {"$ref": "grandchild.json"} - }, - } - }, - "properties": {"dummy": {"type": "string"}}, - }, - root / "child.json", - ) - preprocess_schemas.save_json( - { - "$id": "https://ucp.dev/schemas/grandchild.json", - "title": "Grandchild", - "type": "object", - "properties": { - "value": { - "type": "string", - "ucp_request": {"create": "required"}, - } - }, - }, - root / "grandchild.json", - ) - preprocess_schemas.save_json( - { - "$id": "https://ucp.dev/schemas/parent.json", - "title": "Parent", - "allOf": [{"$ref": "ucp.json#/$defs/entity"}], - "properties": { - "child_item": { - "$ref": "child.json#/$defs/item", - "ucp_request": {"create": "required"}, - } - }, - }, - root / "parent.json", - ) - - with ( - mock.patch.object( - sys, - "argv", - ["preprocess_schemas.py", str(root)], - ), - contextlib.redirect_stdout(io.StringIO()), - ): - preprocess_schemas.main() - - self.assertTrue( - (root / "child_create_request.json").exists(), - "child_create_request.json was not generated", - ) - self.assertTrue( - (root / "grandchild_create_request.json").exists(), - "grandchild_create_request.json was not generated", - ) - - parent_variant = preprocess_schemas.load_json( - root / "parent_create_request.json" - ) - self.assertEqual( - parent_variant["properties"]["child_item"]["$ref"], - "child_create_request.json#/$defs/item", - ) - - -class MetadataUnionTest(unittest.TestCase): - """The UcpMetadata root union is derived from ucp.json $defs.""" - - def test_includes_profiles_and_all_response_schemas(self) -> None: - """Profiles and every response_*_schema belong to the union.""" - ucp = { - "$defs": { - "version": {"type": "string"}, - "version_constraint": {"type": "object"}, - "requires": {"type": "object"}, - "entity": {"type": "object"}, - "base": {"type": "object"}, - "success": {"type": "object"}, - "error": {"type": "object"}, - "platform_schema": {"type": "object"}, - "business_schema": {"type": "object"}, - "response_checkout_schema": {"type": "object"}, - "response_order_schema": {"type": "object"}, - "response_cart_schema": {"type": "object"}, - "response_catalog_schema": {"type": "object"}, - } - } - self.assertEqual( - preprocess_schemas.metadata_union_members(ucp), - [ - "platform_schema", - "business_schema", - "response_checkout_schema", - "response_order_schema", - "response_cart_schema", - "response_catalog_schema", - ], - ) - - def test_picks_up_new_response_types_automatically(self) -> None: - """A response schema added upstream is included without code changes.""" - ucp = { - "$defs": { - "platform_schema": {"type": "object"}, - "business_schema": {"type": "object"}, - "response_invoice_schema": {"type": "object"}, - } - } - self.assertEqual( - preprocess_schemas.metadata_union_members(ucp), - ["platform_schema", "business_schema", "response_invoice_schema"], - ) - - def test_excludes_non_schema_defs(self) -> None: - """Helper and shared defs never leak into the metadata union.""" - ucp = { - "$defs": { - "entity": {"type": "object"}, - "request_schema": {"type": "object"}, - "base": {"type": "object"}, - } - } - self.assertEqual(preprocess_schemas.metadata_union_members(ucp), []) - - def test_empty_defs_yields_empty_union(self) -> None: - """No $defs means no union members.""" - self.assertEqual(preprocess_schemas.metadata_union_members({}), []) - - @unittest.skipUnless( HAVE_SDK, "requires the installed package (pip install -e .)" ) From b55beea9d6166843b6ce4737d2d059841fd9aa13 Mon Sep 17 00:00:00 2001 From: Greg Smith Date: Thu, 10 Sep 2026 05:57:31 +0000 Subject: [PATCH 2/4] fix(codegen): support version argument and prettier-format shopping.openapi.json - Ensure generate_models.sh resolves version arguments (e.g. 2026-08-25) to embedded shopping.openapi.json - Format shopping.openapi.json with prettier to comply with pre-commit --- generate_models.sh | 24 +- shopping.openapi.json | 895 +++++++++--------------------------------- 2 files changed, 205 insertions(+), 714 deletions(-) diff --git a/generate_models.sh b/generate_models.sh index a35c1b5..7a3d96b 100755 --- a/generate_models.sh +++ b/generate_models.sh @@ -29,17 +29,19 @@ if ! command -v uv &> /dev/null; then fi # Input OpenAPI Specification (default: embedded shopping.openapi.json) -OPENAPI_SPEC="${1:-shopping.openapi.json}" - -if [ ! -f "$OPENAPI_SPEC" ]; then - if [ -f "../ucp-schema/dist/shopping.openapi.json" ]; then - OPENAPI_SPEC="../ucp-schema/dist/shopping.openapi.json" - elif [ -f "dist/shopping.openapi.json" ]; then - OPENAPI_SPEC="dist/shopping.openapi.json" - else - echo "Error: OpenAPI spec not found at $OPENAPI_SPEC" - exit 1 - fi +INPUT_ARG="${1:-shopping.openapi.json}" + +if [ -f "$INPUT_ARG" ]; then + OPENAPI_SPEC="$INPUT_ARG" +elif [ -f "shopping.openapi.json" ]; then + OPENAPI_SPEC="shopping.openapi.json" +elif [ -f "../ucp-schema/dist/shopping.openapi.json" ]; then + OPENAPI_SPEC="../ucp-schema/dist/shopping.openapi.json" +elif [ -f "dist/shopping.openapi.json" ]; then + OPENAPI_SPEC="dist/shopping.openapi.json" +else + echo "Error: OpenAPI spec not found at $INPUT_ARG" + exit 1 fi # Output directory and target models file diff --git a/shopping.openapi.json b/shopping.openapi.json index ea1ed15..fcf8a5c 100644 --- a/shopping.openapi.json +++ b/shopping.openapi.json @@ -10,9 +10,7 @@ "operationId": "getMerchantProfile", "summary": "Get Merchant Profile", "description": "Fetch the merchant discovery profile and advertised capabilities.", - "tags": [ - "Discovery" - ], + "tags": ["Discovery"], "parameters": [], "responses": { "200": { @@ -33,9 +31,7 @@ "operationId": "createCart", "summary": "Create a new Cart resource", "description": "Create a new Cart session or resource.", - "tags": [ - "Cart" - ], + "tags": ["Cart"], "parameters": [ { "$ref": "#/components/parameters/UcpAgent" @@ -104,9 +100,7 @@ "operationId": "getCart", "summary": "Get Cart by ID", "description": "Retrieve an existing Cart by its identifier.", - "tags": [ - "Cart" - ], + "tags": ["Cart"], "parameters": [ { "name": "id", @@ -161,9 +155,7 @@ "operationId": "updateCart", "summary": "Update existing Cart", "description": "Update an active Cart session or resource.", - "tags": [ - "Cart" - ], + "tags": ["Cart"], "parameters": [ { "name": "id", @@ -244,9 +236,7 @@ "operationId": "cancelCart", "summary": "Cancel Cart Session", "description": "Cancel an active cart session.", - "tags": [ - "Cart" - ], + "tags": ["Cart"], "parameters": [ { "name": "id", @@ -316,9 +306,7 @@ "operationId": "lookupCatalog", "summary": "Lookup Catalog", "description": "Request body for catalog lookup.", - "tags": [ - "Catalog" - ], + "tags": ["Catalog"], "parameters": [ { "$ref": "#/components/parameters/UcpAgent" @@ -371,9 +359,7 @@ "operationId": "getCatalogProduct", "summary": "Get Product Details", "description": "Request body for single-product retrieval. Supports interactive variant narrowing via selected and preferences.", - "tags": [ - "Catalog" - ], + "tags": ["Catalog"], "parameters": [ { "name": "id", @@ -424,9 +410,7 @@ "operationId": "searchCatalog", "summary": "Search Catalog", "description": "Execute search operation on catalog.", - "tags": [ - "Catalog" - ], + "tags": ["Catalog"], "parameters": [ { "$ref": "#/components/parameters/UcpAgent" @@ -479,9 +463,7 @@ "operationId": "createCheckout", "summary": "Create a new Checkout resource", "description": "Create a new Checkout session or resource.", - "tags": [ - "Checkout" - ], + "tags": ["Checkout"], "parameters": [ { "$ref": "#/components/parameters/UcpAgent" @@ -550,9 +532,7 @@ "operationId": "getCheckout", "summary": "Get Checkout by ID", "description": "Retrieve an existing Checkout by its identifier.", - "tags": [ - "Checkout" - ], + "tags": ["Checkout"], "parameters": [ { "name": "id", @@ -607,9 +587,7 @@ "operationId": "updateCheckout", "summary": "Update existing Checkout", "description": "Update an active Checkout session or resource.", - "tags": [ - "Checkout" - ], + "tags": ["Checkout"], "parameters": [ { "name": "id", @@ -690,9 +668,7 @@ "operationId": "cancelCheckout", "summary": "Cancel Checkout Session", "description": "Cancel an active checkout session.", - "tags": [ - "Checkout" - ], + "tags": ["Checkout"], "parameters": [ { "name": "id", @@ -762,9 +738,7 @@ "operationId": "completeCheckout", "summary": "Complete Checkout Session", "description": "Finalize and complete an active checkout session.", - "tags": [ - "Checkout" - ], + "tags": ["Checkout"], "parameters": [ { "name": "id", @@ -845,9 +819,7 @@ "operationId": "createOrder", "summary": "Create a new Order resource", "description": "Create a new Order session or resource.", - "tags": [ - "Order" - ], + "tags": ["Order"], "parameters": [ { "$ref": "#/components/parameters/UcpAgent" @@ -916,9 +888,7 @@ "operationId": "getOrder", "summary": "Get Order by ID", "description": "Retrieve an existing Order by its identifier.", - "tags": [ - "Order" - ], + "tags": ["Order"], "parameters": [ { "name": "id", @@ -973,9 +943,7 @@ "operationId": "updateOrder", "summary": "Update existing Order", "description": "Update an active Order session or resource.", - "tags": [ - "Order" - ], + "tags": ["Order"], "parameters": [ { "name": "id", @@ -1087,21 +1055,14 @@ }, "status": { "type": "string", - "enum": [ - "pending", - "completed", - "failed" - ], + "enum": ["pending", "completed", "failed"], "description": "Adjustment status." }, "line_items": { "type": "array", "items": { "type": "object", - "required": [ - "id", - "quantity" - ], + "required": ["id", "quantity"], "properties": { "id": { "type": "string", @@ -1133,12 +1094,7 @@ "description": "Human-readable reason or description (e.g., 'Defective item', 'Customer requested')." } }, - "required": [ - "id", - "type", - "occurred_at", - "status" - ], + "required": ["id", "type", "occurred_at", "status"], "title": "Adjustment", "description": "Post-order event that exists independently of fulfillment. Typically represents money movements but can be any post-order change. Polymorphic type that can optionally reference line items.", "type": "object" @@ -1146,10 +1102,7 @@ "Allocation": { "type": "object", "description": "Breakdown of how a discount amount was allocated to a specific target.", - "required": [ - "path", - "amount" - ], + "required": ["path", "amount"], "properties": { "path": { "type": "string", @@ -1171,10 +1124,7 @@ "AppliedDiscount": { "type": "object", "description": "A discount that was successfully applied.", - "required": [ - "title", - "amount" - ], + "required": ["title", "amount"], "properties": { "code": { "type": "string", @@ -1195,10 +1145,7 @@ }, "method": { "type": "string", - "enum": [ - "each", - "across" - ], + "enum": ["each", "across"], "description": "Allocation method. 'each' = applied independently per item. 'across' = split proportionally by value." }, "priority": { @@ -1259,9 +1206,7 @@ "description": "A Constraint Expression describing the instrument this entry makes available. Keys in `properties` name members of the `constraint_target` declared by the instrument schema for this `type`. Requirements on submitted request data belong in `ucp.request_constraints` instead." } }, - "required": [ - "type" - ], + "required": ["type"], "title": "Available Payment Instrument", "description": "An instrument type available from a payment handler with optional constraints.", "type": "object" @@ -1273,9 +1218,7 @@ "description": "Method types that permit multiple destinations within one cart (e.g. split shipping across addresses). Listing a method permits it; an omitted method does not. Open — businesses MAY list any method type.", "items": { "type": "object", - "required": [ - "method" - ], + "required": ["method"], "additionalProperties": true, "properties": { "method": { @@ -1355,9 +1298,7 @@ "$ref": "#/components/schemas/CapabilityBase" }, { - "required": [ - "schema" - ] + "required": ["schema"] } ] }, @@ -1369,10 +1310,7 @@ "$ref": "#/components/schemas/CapabilityBase" }, { - "required": [ - "spec", - "schema" - ] + "required": ["spec", "schema"] } ] }, @@ -1386,13 +1324,7 @@ ] }, "Cart": { - "required": [ - "ucp", - "id", - "line_items", - "currency", - "totals" - ], + "required": ["ucp", "id", "line_items", "currency", "totals"], "additionalProperties": true, "type": "object", "title": "Cart", @@ -1475,9 +1407,7 @@ } }, "CartCreateRequest": { - "required": [ - "line_items" - ], + "required": ["line_items"], "additionalProperties": true, "type": "object", "title": "CartCreateRequest", @@ -1510,9 +1440,7 @@ } }, "CartUpdateRequest": { - "required": [ - "line_items" - ], + "required": ["line_items"], "additionalProperties": true, "type": "object", "title": "CartUpdateRequest", @@ -1564,9 +1492,7 @@ "description": "A fulfillment method on a catalog variant: how the variant can be fulfilled, and its availability.", "type": "object", "additionalProperties": true, - "required": [ - "type" - ], + "required": ["type"], "properties": { "type": { "type": "string", @@ -1604,9 +1530,7 @@ "description": "Source taxonomy. Well-known values: `google_product_category`, `shopify`, `merchant`." } }, - "required": [ - "value" - ], + "required": ["value"], "title": "Category", "description": "A product category with optional taxonomy identifier.", "type": "object" @@ -1725,9 +1649,7 @@ "type": "object" }, "CheckoutCompleteRequest": { - "required": [ - "payment" - ], + "required": ["payment"], "properties": { "buyer": { "$ref": "#/components/schemas/Buyer", @@ -1749,9 +1671,7 @@ "type": "object" }, "CheckoutCreateRequest": { - "required": [ - "line_items" - ], + "required": ["line_items"], "properties": { "line_items": { "type": "array", @@ -1790,9 +1710,7 @@ "type": "object" }, "CheckoutUpdateRequest": { - "required": [ - "line_items" - ], + "required": ["line_items"], "properties": { "line_items": { "type": "array", @@ -1833,9 +1751,7 @@ "Config": { "type": "object", "description": "Business browser endpoint configuration for shopping permalinks.", - "required": [ - "endpoint" - ], + "required": ["endpoint"], "properties": { "endpoint": { "$ref": "#/components/schemas/Endpoint" @@ -1856,11 +1772,7 @@ "ConsentPurpose": { "type": "object", "description": "A buyer's consent decision for a purpose (e.g., marketing, analytics). Carries the current binary state, its source (business default or platform-captured buyer decision), human-readable context, and optional refinements scoping the decision to specific channels, vendors, or programs.", - "required": [ - "granted", - "source", - "description" - ], + "required": ["granted", "source", "description"], "properties": { "granted": { "type": "boolean", @@ -1868,10 +1780,7 @@ }, "source": { "type": "string", - "enum": [ - "business", - "platform" - ], + "enum": ["business", "platform"], "description": "Identifies the party that asserted the current `granted` value. `business` means the value reflects the business's default policy; `platform` means the value reflects an explicit buyer decision captured by the platform." }, "description": { @@ -1900,11 +1809,7 @@ "ConsentSegment": { "type": "object", "description": "A buyer's consent decision for a specific refinement of a parent purpose (e.g., email marketing under the marketing purpose). Overrides the parent's `granted` value for this scope. Segments do not nest further.", - "required": [ - "granted", - "source", - "description" - ], + "required": ["granted", "source", "description"], "properties": { "granted": { "type": "boolean", @@ -1912,10 +1817,7 @@ }, "source": { "type": "string", - "enum": [ - "business", - "platform" - ], + "enum": ["business", "platform"], "description": "Identifies the party that asserted the current `granted` value for this segment. `business` means the value reflects the business's default policy; `platform` means the value reflects an explicit buyer decision captured by the platform." }, "description": { @@ -2010,9 +1912,7 @@ "description": "Buyer-preferred payment handlers in priority order (most preferred first). Each entry names a handler advertised in the Business profile's `ucp.payment_handlers`, optionally narrowed to preferred instrument types. The Business SHOULD use it to preselect or prioritize the handler (and type, when given) and MAY ignore unavailable or ineligible entries; unrecognized values MUST be ignored without error.", "items": { "type": "object", - "required": [ - "handler" - ], + "required": ["handler"], "properties": { "handler": { "$ref": "#/components/schemas/ReverseDomainName", @@ -2096,10 +1996,7 @@ "type": "array", "items": { "type": "object", - "required": [ - "name", - "values" - ], + "required": ["name", "values"], "properties": { "name": { "type": "string" @@ -2154,10 +2051,7 @@ "type": "array", "items": { "type": "string", - "enum": [ - "light", - "dark" - ] + "enum": ["light", "dark"] }, "description": "Color schemes the business supports. Hosts use ec_color_scheme query parameter to request a scheme from this list." } @@ -2186,9 +2080,7 @@ "type": "string" } }, - "required": [ - "status" - ] + "required": ["status"] } ] }, @@ -2231,10 +2123,7 @@ "title": "Error Response", "description": "Generic error response when business logic prevents resource creation or failed to retrieve resource. Used when no valid resource can be established.", "type": "object", - "required": [ - "ucp", - "messages" - ] + "required": ["ucp", "messages"] }, "Expectation": { "properties": { @@ -2246,10 +2135,7 @@ "type": "array", "items": { "type": "object", - "required": [ - "id", - "quantity" - ], + "required": ["id", "quantity"], "properties": { "id": { "type": "string", @@ -2282,12 +2168,7 @@ "description": "When this expectation can be fulfilled: 'now' or ISO 8601 timestamp for future date (backorder, pre-order)." } }, - "required": [ - "id", - "line_items", - "method_type", - "destination" - ], + "required": ["id", "line_items", "method_type", "destination"], "title": "Expectation", "description": "Buyer-facing fulfillment expectation representing logical groupings of items (e.g., 'package'). Can be split, merged, or adjusted post-order to set buyer expectations for when/how items arrive.", "type": "object" @@ -2314,10 +2195,7 @@ "description": "Container for fulfillment methods and availability." }, "FulfillmentAvailableMethod": { - "required": [ - "type", - "line_item_ids" - ], + "required": ["type", "line_item_ids"], "properties": { "type": { "type": "string", @@ -2331,10 +2209,7 @@ } }, "fulfillable_on": { - "type": [ - "string", - "null" - ], + "type": ["string", "null"], "description": "'now' for immediate availability, or ISO 8601 date for future (preorders, transfers)." }, "description": { @@ -2480,10 +2355,7 @@ "type": "array", "items": { "type": "object", - "required": [ - "id", - "quantity" - ], + "required": ["id", "quantity"], "properties": { "id": { "type": "string", @@ -2517,12 +2389,7 @@ "description": "Human-readable description of the shipment status or delivery information (e.g., 'Delivered to front door', 'Out for delivery')." } }, - "required": [ - "id", - "occurred_at", - "type", - "line_items" - ], + "required": ["id", "occurred_at", "type", "line_items"], "title": "Fulfillment Event", "description": "Append-only fulfillment event representing an actual shipment. References line items by ID.", "type": "object" @@ -2558,10 +2425,7 @@ ] }, "FulfillmentGroup": { - "required": [ - "id", - "line_item_ids" - ], + "required": ["id", "line_item_ids"], "properties": { "id": { "type": "string", @@ -2582,10 +2446,7 @@ } }, "selected_option_id": { - "type": [ - "string", - "null" - ], + "type": ["string", "null"], "description": "ID of the selected fulfillment option for this group." } }, @@ -2598,10 +2459,7 @@ "additionalProperties": true, "properties": { "selected_option_id": { - "type": [ - "string", - "null" - ], + "type": ["string", "null"], "description": "ID of the selected fulfillment option for this group." } }, @@ -2610,19 +2468,14 @@ "type": "object" }, "FulfillmentGroupUpdateRequest": { - "required": [ - "id" - ], + "required": ["id"], "properties": { "id": { "type": "string", "description": "Group identifier for referencing merchant-generated groups in updates." }, "selected_option_id": { - "type": [ - "string", - "null" - ], + "type": ["string", "null"], "description": "ID of the selected fulfillment option for this group." } }, @@ -2700,15 +2553,9 @@ ] }, "FulfillmentMethod": { - "required": [ - "id", - "type", - "line_item_ids" - ], + "required": ["id", "type", "line_item_ids"], "dependentRequired": { - "destinations": [ - "type" - ] + "destinations": ["type"] }, "title": "Fulfillment Method", "description": "A fulfillment method with destinations and groups.", @@ -2737,10 +2584,7 @@ } }, "selected_destination_id": { - "type": [ - "string", - "null" - ], + "type": ["string", "null"], "description": "ID of the selected destination. Accepts any stable, Business-scoped ID the Business recognizes for this method, including Location IDs not yet enumerated in `destinations`." }, "groups": { @@ -2761,9 +2605,7 @@ "type": "string" } }, - "required": [ - "type" - ] + "required": ["type"] }, "then": { "properties": { @@ -2786,9 +2628,7 @@ "type": "string" } }, - "required": [ - "type" - ] + "required": ["type"] }, "then": { "properties": { @@ -2805,13 +2645,9 @@ ] }, "FulfillmentMethodCreateRequest": { - "required": [ - "type" - ], + "required": ["type"], "dependentRequired": { - "destinations": [ - "type" - ] + "destinations": ["type"] }, "title": "FulfillmentMethodCreateRequest", "description": "Request payload to create a new FulfillmentMethod. A fulfillment method with destinations and groups.", @@ -2822,10 +2658,7 @@ "description": "Fulfillment method type. Well-known values: `shipping`, `pickup`. Businesses MAY use additional values." }, "selected_destination_id": { - "type": [ - "string", - "null" - ], + "type": ["string", "null"], "description": "ID of the selected destination. Accepts any stable, Business-scoped ID the Business recognizes for this method, including Location IDs not yet enumerated in `destinations`." }, "groups": { @@ -2846,9 +2679,7 @@ "type": "string" } }, - "required": [ - "type" - ] + "required": ["type"] }, "then": { "properties": { @@ -2871,9 +2702,7 @@ "type": "string" } }, - "required": [ - "type" - ] + "required": ["type"] }, "then": { "properties": {} @@ -2882,13 +2711,9 @@ ] }, "FulfillmentMethodUpdateRequest": { - "required": [ - "line_item_ids" - ], + "required": ["line_item_ids"], "dependentRequired": { - "destinations": [ - "type" - ] + "destinations": ["type"] }, "title": "FulfillmentMethodUpdateRequest", "description": "Request payload to update an existing FulfillmentMethod. A fulfillment method with destinations and groups.", @@ -2910,10 +2735,7 @@ } }, "selected_destination_id": { - "type": [ - "string", - "null" - ], + "type": ["string", "null"], "description": "ID of the selected destination. Accepts any stable, Business-scoped ID the Business recognizes for this method, including Location IDs not yet enumerated in `destinations`." }, "groups": { @@ -2934,9 +2756,7 @@ "type": "string" } }, - "required": [ - "type" - ] + "required": ["type"] }, "then": { "properties": { @@ -2959,9 +2779,7 @@ "type": "string" } }, - "required": [ - "type" - ] + "required": ["type"] }, "then": { "properties": {} @@ -3000,9 +2818,7 @@ "description": "Fulfillment option totals breakdown." } }, - "required": [ - "totals" - ] + "required": ["totals"] } ], "type": "object", @@ -3010,10 +2826,7 @@ "description": "A fulfillment option within a group (e.g., Standard Shipping $5, Express $15). Extends the fulfillment option base with cost and timing." }, "FulfillmentOptionBase": { - "required": [ - "id", - "title" - ], + "required": ["id", "title"], "properties": { "id": { "type": "string", @@ -3172,9 +2985,7 @@ "GetProductRequest": { "type": "object", "description": "Request body for single-product retrieval. Supports interactive variant narrowing via selected and preferences.", - "required": [ - "id" - ], + "required": ["id"], "properties": { "id": { "type": "string", @@ -3211,10 +3022,7 @@ }, "GetProductResponse": { "type": "object", - "required": [ - "ucp", - "product" - ], + "required": ["ucp", "product"], "properties": { "ucp": { "$ref": "#/components/schemas/ResponseCatalogSchema" @@ -3263,15 +3071,10 @@ "match": { "type": "string", "description": "How the request identifier resolved to this variant. Well-known values: `exact` (input directly identifies this variant, e.g., variant ID, SKU), `featured` (server selected this variant as representative, e.g., product ID resolved to best match). Businesses MAY implement and provide additional resolution strategies.", - "examples": [ - "exact", - "featured" - ] + "examples": ["exact", "featured"] } }, - "required": [ - "id" - ], + "required": ["id"], "title": "Input Correlation", "description": "Maps a request identifier to the variant it resolved to, with match semantics.", "type": "object" @@ -3279,9 +3082,7 @@ "Instance": { "type": "object", "description": "Common fields for one outstanding Action instance are id and optional config. The extension declaring the Action type defines type-specific processing data under config. Additional properties are permitted for forward compatibility.", - "required": [ - "id" - ], + "required": ["id"], "additionalProperties": true, "properties": { "id": { @@ -3297,11 +3098,7 @@ } }, "Item": { - "required": [ - "id", - "title", - "price" - ], + "required": ["id", "title", "price"], "properties": { "id": { "type": "string", @@ -3333,9 +3130,7 @@ "type": "object" }, "ItemCreateRequest": { - "required": [ - "id" - ], + "required": ["id"], "properties": { "id": { "type": "string", @@ -3351,9 +3146,7 @@ "description": "Request payload to create a new Item." }, "ItemUpdateRequest": { - "required": [ - "id" - ], + "required": ["id"], "properties": { "id": { "type": "string", @@ -3371,10 +3164,7 @@ "JwkPublicKey": { "type": "object", "description": "Public JSON Web Key used for HTTP Message Signatures and signed webhook verification. UCP profiles publish public keys only; private key material MUST NOT appear in a profile. Well-known key types: EC (ECDSA P-256, P-384) and OKP (EdDSA Ed25519); OKP keys are RECOMMENDED for signers opting into Web Bot Auth (WBA) interop on HTTP transport. A single profile MAY publish keys of either or both types; consumers select keys by kid. The kty, crv, and alg vocabularies are OPEN: verifiers MUST tolerate key types, curves, and algorithms they do not recognize, selecting keys by kid at verification time. An unsupported key affects only the signature that references it (algorithm_unsupported) and MUST NOT cause whole-profile rejection. Additional public JWK members are permitted; consumers ignore unknown members.", - "required": [ - "kid", - "kty" - ], + "required": ["kid", "kty"], "properties": { "kid": { "type": "string", @@ -3382,19 +3172,12 @@ }, "kty": { "type": "string", - "examples": [ - "EC", - "OKP" - ], + "examples": ["EC", "OKP"], "description": "JWK key type. Well-known values: EC for ECDSA (P-256, P-384); OKP for EdDSA (Ed25519). Open vocabulary; verifiers tolerate unrecognized types and select keys by kid." }, "crv": { "type": "string", - "examples": [ - "P-256", - "P-384", - "Ed25519" - ], + "examples": ["P-256", "P-384", "Ed25519"], "description": "Curve name. Well-known values: P-256, P-384 (EC); Ed25519 (OKP). Open vocabulary." }, "x": { @@ -3407,11 +3190,7 @@ }, "alg": { "type": "string", - "examples": [ - "ES256", - "ES384", - "EdDSA" - ], + "examples": ["ES256", "ES384", "EdDSA"], "description": "JWA algorithm associated with this public key. Optional; verifiers derive the algorithm from crv when alg is omitted. When present for a well-known curve it MUST match: ES256 with P-256, ES384 with P-384, EdDSA with Ed25519." }, "use": { @@ -3430,16 +3209,10 @@ "type": "string" } }, - "required": [ - "kty" - ] + "required": ["kty"] }, "then": { - "required": [ - "crv", - "x", - "y" - ] + "required": ["crv", "x", "y"] } }, { @@ -3452,15 +3225,10 @@ "type": "string" } }, - "required": [ - "kty" - ] + "required": ["kty"] }, "then": { - "required": [ - "crv", - "x" - ] + "required": ["crv", "x"] } }, { @@ -3473,9 +3241,7 @@ "type": "string" } }, - "required": [ - "crv" - ] + "required": ["crv"] }, "then": { "properties": { @@ -3497,9 +3263,7 @@ "type": "string" } }, - "required": [ - "crv" - ] + "required": ["crv"] }, "then": { "properties": { @@ -3521,9 +3285,7 @@ "type": "string" } }, - "required": [ - "crv" - ] + "required": ["crv"] }, "then": { "properties": { @@ -3539,56 +3301,35 @@ "not": { "anyOf": [ { - "required": [ - "d" - ] + "required": ["d"] }, { - "required": [ - "p" - ] + "required": ["p"] }, { - "required": [ - "q" - ] + "required": ["q"] }, { - "required": [ - "dp" - ] + "required": ["dp"] }, { - "required": [ - "dq" - ] + "required": ["dq"] }, { - "required": [ - "qi" - ] + "required": ["qi"] }, { - "required": [ - "oth" - ] + "required": ["oth"] }, { - "required": [ - "k" - ] + "required": ["k"] } ] }, "additionalProperties": true }, "LineItem": { - "required": [ - "id", - "item", - "quantity", - "totals" - ], + "required": ["id", "item", "quantity", "totals"], "properties": { "id": { "type": "string" @@ -3619,10 +3360,7 @@ "description": "Line item object. Expected to use the currency of the parent object." }, "LineItemCreateRequest": { - "required": [ - "item", - "quantity" - ], + "required": ["item", "quantity"], "properties": { "item": { "$ref": "#/components/schemas/ItemCreateRequest" @@ -3639,10 +3377,7 @@ "description": "Request payload to create a new LineItem. Line item object. Expected to use the currency of the parent object." }, "LineItemUpdateRequest": { - "required": [ - "item", - "quantity" - ], + "required": ["item", "quantity"], "properties": { "id": { "type": "string" @@ -3681,10 +3416,7 @@ "description": "Optional display text for the link. When provided, use this instead of generating from type." } }, - "required": [ - "type", - "url" - ], + "required": ["type", "url"], "title": "Link", "type": "object" }, @@ -3723,9 +3455,7 @@ "default": "business_location" } }, - "required": [ - "type" - ] + "required": ["type"] } ], "type": "object", @@ -3761,10 +3491,7 @@ "description": "Request payload to update an existing LocationDestination. A business location fulfillment destination. Business-authored and response-only: the Platform selects a location via `selected_destination_id` rather than writing destinations." }, "LocationSummary": { - "required": [ - "id", - "name" - ], + "required": ["id", "name"], "properties": { "id": { "type": "string", @@ -3785,9 +3512,7 @@ "additionalProperties": true }, "LocationSummaryCreateRequest": { - "required": [ - "id" - ], + "required": ["id"], "properties": { "id": { "type": "string", @@ -3800,9 +3525,7 @@ "additionalProperties": true }, "LocationSummaryUpdateRequest": { - "required": [ - "id" - ], + "required": ["id"], "properties": { "id": { "type": "string", @@ -3817,9 +3540,7 @@ "LookupRequest": { "type": "object", "description": "Request body for catalog lookup.", - "required": [ - "ids" - ], + "required": ["ids"], "properties": { "ids": { "type": "array", @@ -3846,10 +3567,7 @@ }, "LookupResponse": { "type": "object", - "required": [ - "ucp", - "products" - ], + "required": ["ucp", "products"], "properties": { "ucp": { "$ref": "#/components/schemas/ResponseCatalogSchema" @@ -3901,9 +3619,7 @@ "$ref": "#/components/schemas/Variant" }, { - "required": [ - "inputs" - ], + "required": ["inputs"], "properties": { "inputs": { "type": "array", @@ -3934,9 +3650,7 @@ }, { "type": "object", - "required": [ - "value" - ], + "required": ["value"], "properties": { "value": { "type": "integer", @@ -3976,10 +3690,7 @@ "description": "Height in pixels (for images/video)." } }, - "required": [ - "type", - "url" - ], + "required": ["type", "url"], "title": "Media", "description": "Media item (image, video, etc.).", "type": "object" @@ -4038,10 +3749,7 @@ }, "content_type": { "type": "string", - "enum": [ - "plain", - "markdown" - ], + "enum": ["plain", "markdown"], "default": "plain", "description": "Content format, default = plain." }, @@ -4060,12 +3768,7 @@ "description": "Reflects the resource state and recommended action. 'recoverable': platform can resolve the condition in band, for example by modifying inputs or processing a related Action, and submit a new operation when needed. 'requires_buyer_input': merchant requires information their API doesn't support collecting programmatically (checkout incomplete). 'requires_buyer_review': buyer must authorize before order placement due to policy, regulatory, or entitlement rules. 'unrecoverable': no valid resource exists to act on, retry with new resource or inputs. Errors with 'requires_*' severity contribute to 'status: requires_escalation'." } }, - "required": [ - "type", - "code", - "content", - "severity" - ], + "required": ["type", "code", "content", "severity"], "title": "Message Error", "type": "object" }, @@ -4086,10 +3789,7 @@ }, "content_type": { "type": "string", - "enum": [ - "plain", - "markdown" - ], + "enum": ["plain", "markdown"], "default": "plain", "description": "Content format, default = plain." }, @@ -4098,10 +3798,7 @@ "description": "Human-readable message." } }, - "required": [ - "type", - "content" - ], + "required": ["type", "content"], "title": "Message Info", "type": "object" }, @@ -4126,10 +3823,7 @@ }, "content_type": { "type": "string", - "enum": [ - "plain", - "markdown" - ], + "enum": ["plain", "markdown"], "default": "plain", "description": "Content format, default = plain." }, @@ -4149,11 +3843,7 @@ "description": "Reference URL for more information (e.g., regulatory site, registry entry, policy page)." } }, - "required": [ - "type", - "code", - "content" - ], + "required": ["type", "code", "content"], "title": "Message Warning", "type": "object" }, @@ -4168,9 +3858,7 @@ "description": "Display text for this option value (e.g., 'Small', 'Blue')." } }, - "required": [ - "label" - ], + "required": ["label"], "title": "Option Value", "description": "A selectable value for a product option.", "type": "object" @@ -4288,10 +3976,7 @@ "description": "Permalink to access the order on merchant site." } }, - "required": [ - "id", - "permalink_url" - ], + "required": ["id", "permalink_url"], "title": "Order Confirmation", "description": "Order details available at the time of checkout completion.", "type": "object" @@ -4389,10 +4074,7 @@ }, "quantity": { "type": "object", - "required": [ - "total", - "fulfilled" - ], + "required": ["total", "fulfilled"], "properties": { "original": { "type": "integer", @@ -4424,12 +4106,7 @@ }, "status": { "type": "string", - "enum": [ - "processing", - "partial", - "fulfilled", - "removed" - ], + "enum": ["processing", "partial", "fulfilled", "removed"], "description": "Derived status: removed if quantity.total == 0, fulfilled if quantity.total > 0 and quantity.fulfilled == quantity.total, partial if quantity.total > 0 and quantity.fulfilled > 0, otherwise processing." }, "parent_id": { @@ -4437,13 +4114,7 @@ "description": "Parent line item identifier for any nested structures." } }, - "required": [ - "id", - "item", - "quantity", - "totals", - "status" - ], + "required": ["id", "item", "quantity", "totals", "status"], "title": "Order Line Item", "type": "object" }, @@ -4451,9 +4122,7 @@ "title": "Platform Order Schema", "description": "Platform's order capability configuration.", "type": "object", - "required": [ - "webhook_url" - ], + "required": ["webhook_url"], "properties": { "webhook_url": { "type": "string", @@ -4568,9 +4237,7 @@ "title": "Payment Credential", "description": "The base definition for any payment credential. Handlers define specific credential types.", "type": "object", - "required": [ - "type" - ] + "required": ["type"] }, "PaymentHandlerBase": { "allOf": [ @@ -4579,9 +4246,7 @@ }, { "type": "object", - "required": [ - "id" - ] + "required": ["id"] }, { "type": "object", @@ -4615,10 +4280,7 @@ "$ref": "#/components/schemas/PaymentHandlerBase" }, { - "required": [ - "spec", - "schema" - ] + "required": ["spec", "schema"] } ] }, @@ -4661,11 +4323,7 @@ "title": "Payment Instrument", "description": "The base definition for any payment instrument. It links the instrument to a specific payment handler.", "type": "object", - "required": [ - "id", - "handler_id", - "type" - ] + "required": ["id", "handler_id", "type"] }, "PlatformFulfillmentConfig": { "properties": { @@ -4706,10 +4364,7 @@ "title": "Policy", "description": "A durable business rule about the items in a response — return/refund terms, warranty, and the like — at the time of purchase. Every policy carries a `type` (an open reverse-DNS vocabulary) and a `description` so a platform can present it without understanding its type-specific fields; type-specific fields (gated by `type`) add structured context for platforms that model that type. Policies are reference data; the obligation to display a term to the buyer is carried by a `messages[]` warning whose `code` equals the policy `type` — see the Policies section of the specification.", "type": "object", - "required": [ - "type", - "description" - ] + "required": ["type", "description"] }, "PostalAddress": { "properties": { @@ -4765,10 +4420,7 @@ "pattern": "^[A-Z]{3}$" } }, - "required": [ - "amount", - "currency" - ], + "required": ["amount", "currency"], "title": "Price", "description": "Price with explicit currency.", "type": "object" @@ -4799,10 +4451,7 @@ "description": "Maximum price in the range." } }, - "required": [ - "min", - "max" - ], + "required": ["min", "max"], "title": "Price Range", "description": "A price range representing minimum and maximum values (e.g., a common example in retail shopping is when prices vary across product variants).", "type": "object" @@ -4883,13 +4532,7 @@ "description": "Business-defined custom data extending the standard product model." } }, - "required": [ - "id", - "title", - "description", - "price_range", - "variants" - ], + "required": ["id", "title", "description", "price_range", "variants"], "title": "Product", "description": "A product in the catalog with variants and options.", "type": "object" @@ -4909,10 +4552,7 @@ "description": "Available values for this option." } }, - "required": [ - "name", - "values" - ], + "required": ["name", "values"], "title": "Product Option", "description": "A product option such as size, color, or material.", "type": "object" @@ -4929,9 +4569,7 @@ "ProfileBase": { "type": "object", "description": "Common wrapper for UCP profile documents.", - "required": [ - "ucp" - ], + "required": ["ucp"], "properties": { "ucp": { "$ref": "#/components/schemas/UcpBase", @@ -5025,10 +4663,7 @@ "description": "Number of reviews contributing to the rating." } }, - "required": [ - "value", - "scale_max" - ], + "required": ["value", "scale_max"], "title": "Rating", "description": "Product rating aggregate.", "type": "object" @@ -5132,9 +4767,7 @@ "description": "Total number of matching items, if available." } }, - "required": [ - "has_next_page" - ], + "required": ["has_next_page"], "if": { "properties": { "has_next_page": { @@ -5143,14 +4776,10 @@ "type": "boolean" } }, - "required": [ - "has_next_page" - ] + "required": ["has_next_page"] }, "then": { - "required": [ - "cursor" - ] + "required": ["cursor"] } }, "ResponseCartSchema": { @@ -5201,9 +4830,7 @@ "$ref": "#/components/schemas/UcpBase" }, { - "required": [ - "payment_handlers" - ], + "required": ["payment_handlers"], "properties": { "services": { "additionalProperties": { @@ -5329,10 +4956,7 @@ }, "SearchResponse": { "type": "object", - "required": [ - "ucp", - "products" - ], + "required": ["ucp", "products"], "properties": { "ucp": { "$ref": "#/components/schemas/ResponseCatalogSchema" @@ -5382,10 +5006,7 @@ "description": "Selected option label (e.g., 'Large')." } }, - "required": [ - "name", - "label" - ], + "required": ["name", "label"], "title": "Selected Option", "description": "A specific option selection on a variant (e.g., Size: Large).", "type": "object" @@ -5415,18 +5036,11 @@ }, { "type": "object", - "required": [ - "transport" - ], + "required": ["transport"], "properties": { "transport": { "type": "string", - "enum": [ - "rest", - "mcp", - "a2a", - "embedded" - ], + "enum": ["rest", "mcp", "a2a", "embedded"], "description": "Transport protocol for this service binding." }, "endpoint": { @@ -5498,9 +5112,7 @@ "$ref": "#/components/schemas/ServiceBase" }, { - "required": [ - "spec" - ] + "required": ["spec"] }, { "anyOf": [ @@ -5615,10 +5227,7 @@ "default": "shipping_address" } }, - "required": [ - "id", - "type" - ] + "required": ["id", "type"] } ], "type": "object", @@ -5716,31 +5325,21 @@ "type": "string" } }, - "required": [ - "status" - ] + "required": ["status"] } ] }, "Total": { - "required": [ - "type", - "amount" - ], + "required": ["type", "amount"], "allOf": [ { "if": { "properties": { "type": { - "enum": [ - "discount", - "items_discount" - ] + "enum": ["discount", "items_discount"] } }, - "required": [ - "type" - ] + "required": ["type"] }, "then": { "properties": { @@ -5754,17 +5353,10 @@ "if": { "properties": { "type": { - "enum": [ - "subtotal", - "fulfillment", - "tax", - "fee" - ] + "enum": ["subtotal", "fulfillment", "tax", "fee"] } }, - "required": [ - "type" - ] + "required": ["type"] }, "then": { "properties": { @@ -5799,15 +5391,10 @@ "if": { "properties": { "type": { - "enum": [ - "discount", - "items_discount" - ] + "enum": ["discount", "items_discount"] } }, - "required": [ - "type" - ] + "required": ["type"] }, "then": { "properties": { @@ -5821,17 +5408,10 @@ "if": { "properties": { "type": { - "enum": [ - "subtotal", - "fulfillment", - "tax", - "fee" - ] + "enum": ["subtotal", "fulfillment", "tax", "fee"] } }, - "required": [ - "type" - ] + "required": ["type"] }, "then": { "properties": { @@ -5853,15 +5433,10 @@ "if": { "properties": { "type": { - "enum": [ - "discount", - "items_discount" - ] + "enum": ["discount", "items_discount"] } }, - "required": [ - "type" - ] + "required": ["type"] }, "then": { "properties": { @@ -5875,17 +5450,10 @@ "if": { "properties": { "type": { - "enum": [ - "subtotal", - "fulfillment", - "tax", - "fee" - ] + "enum": ["subtotal", "fulfillment", "tax", "fee"] } }, - "required": [ - "type" - ] + "required": ["type"] }, "then": { "properties": { @@ -5911,9 +5479,7 @@ "type": "string" } }, - "required": [ - "type" - ] + "required": ["type"] }, "minContains": 1, "maxContains": 1 @@ -5927,9 +5493,7 @@ "type": "string" } }, - "required": [ - "type" - ] + "required": ["type"] }, "minContains": 1, "maxContains": 1 @@ -5957,10 +5521,7 @@ } }, "description": "Sub-line entry. Additional metadata MAY be included.", - "required": [ - "display_text", - "amount" - ] + "required": ["display_text", "amount"] }, "description": "Optional itemized breakdown. The parent entry is always rendered; lines are supplementary. Sum of line amounts MUST equal the parent entry amount." } @@ -5983,14 +5544,10 @@ } } }, - "required": [ - "type" - ] + "required": ["type"] }, "then": { - "required": [ - "display_text" - ] + "required": ["display_text"] } } ] @@ -6010,9 +5567,7 @@ "type": "string" } }, - "required": [ - "type" - ] + "required": ["type"] }, "minContains": 1, "maxContains": 1 @@ -6026,9 +5581,7 @@ "type": "string" } }, - "required": [ - "type" - ] + "required": ["type"] }, "minContains": 1, "maxContains": 1 @@ -6060,14 +5613,10 @@ } } }, - "required": [ - "type" - ] + "required": ["type"] }, "then": { - "required": [ - "display_text" - ] + "required": ["display_text"] } } ] @@ -6087,9 +5636,7 @@ "type": "string" } }, - "required": [ - "type" - ] + "required": ["type"] }, "minContains": 1, "maxContains": 1 @@ -6103,9 +5650,7 @@ "type": "string" } }, - "required": [ - "type" - ] + "required": ["type"] }, "minContains": 1, "maxContains": 1 @@ -6137,14 +5682,10 @@ } } }, - "required": [ - "type" - ] + "required": ["type"] }, "then": { - "required": [ - "display_text" - ] + "required": ["display_text"] } } ] @@ -6154,9 +5695,7 @@ "type": "array" }, "Ucp": { - "required": [ - "version" - ], + "required": ["version"], "type": "object", "title": "UCP Metadata", "description": "Protocol metadata for discovery profiles and responses. Uses slim schema pattern with context-specific required fields.", @@ -6170,10 +5709,7 @@ }, "status": { "type": "string", - "enum": [ - "success", - "error" - ], + "enum": ["success", "error"], "default": "success", "description": "Application-level status of the UCP operation." }, @@ -6221,9 +5757,7 @@ "UcpBase": { "description": "Base UCP metadata with shared properties for all schema types.", "type": "object", - "required": [ - "version" - ], + "required": ["version"], "properties": { "version": { "$ref": "#/components/schemas/Version" @@ -6234,10 +5768,7 @@ }, "status": { "type": "string", - "enum": [ - "success", - "error" - ], + "enum": ["success", "error"], "default": "success", "description": "Application-level status of the UCP operation." }, @@ -6290,10 +5821,7 @@ "$ref": "#/components/schemas/UcpBase" }, { - "required": [ - "services", - "payment_handlers" - ], + "required": ["services", "payment_handlers"], "properties": { "supported_versions": { "type": "object", @@ -6332,9 +5860,7 @@ ] }, "UcpCreateRequest": { - "required": [ - "version" - ], + "required": ["version"], "type": "object", "title": "UcpCreateRequest", "description": "Request payload to create a new Ucp. Protocol metadata for discovery profiles and responses. Uses slim schema pattern with context-specific required fields.", @@ -6344,10 +5870,7 @@ }, "status": { "type": "string", - "enum": [ - "success", - "error" - ], + "enum": ["success", "error"], "default": "success", "description": "Application-level status of the UCP operation." }, @@ -6395,9 +5918,7 @@ "UcpEntity": { "type": "object", "description": "Shared foundation for all UCP entities.", - "required": [ - "version" - ], + "required": ["version"], "properties": { "version": { "$ref": "#/components/schemas/Version", @@ -6432,10 +5953,7 @@ "$ref": "#/components/schemas/UcpBase" }, { - "required": [ - "services", - "payment_handlers" - ], + "required": ["services", "payment_handlers"], "properties": { "services": { "additionalProperties": { @@ -6463,9 +5981,7 @@ ] }, "UcpUpdateRequest": { - "required": [ - "version" - ], + "required": ["version"], "type": "object", "title": "UcpUpdateRequest", "description": "Request payload to update an existing Ucp. Protocol metadata for discovery profiles and responses. Uses slim schema pattern with context-specific required fields.", @@ -6475,10 +5991,7 @@ }, "status": { "type": "string", - "enum": [ - "success", - "error" - ], + "enum": ["success", "error"], "default": "success", "description": "Application-level status of the UCP operation." }, @@ -6534,9 +6047,7 @@ "type": "string" } }, - "required": [ - "unit" - ] + "required": ["unit"] }, "then": { "properties": { @@ -6569,10 +6080,7 @@ "title": "Unit", "description": "A reusable unit descriptor for quantities and measures. Its unit-descriptor machine identity is (`unit`, effective `scale`), where effective `scale` is the provided `scale` or 0; `display_text` is excluded.", "type": "object", - "required": [ - "unit", - "display_text" - ] + "required": ["unit", "display_text"] }, "UnitPrice": { "properties": { @@ -6618,12 +6126,7 @@ ] } }, - "required": [ - "amount", - "currency", - "measure", - "reference" - ], + "required": ["amount", "currency", "measure", "reference"], "title": "Unit Price", "description": "Price per standard unit of measurement. MAY be omitted when unit pricing does not apply. `unit_price.currency` MUST equal `price.currency`; the comparator MUST NOT perform currency conversion. `measure.unit` and `reference.unit` MUST be identical; cross-unit conversion is not permitted. Their scales MAY differ; each value represents `value × 10^-scale`.", "type": "object" @@ -6633,9 +6136,7 @@ "description": "A Value Constraint containing `enum`, `const`, or both.", "anyOf": [ { - "required": [ - "enum" - ], + "required": ["enum"], "type": "object", "properties": { "enum": { @@ -6650,9 +6151,7 @@ "additionalProperties": false }, { - "required": [ - "const" - ], + "required": ["const"], "type": "object", "properties": { "enum": { @@ -6693,10 +6192,7 @@ "type": "array", "items": { "type": "object", - "required": [ - "type", - "value" - ], + "required": ["type", "value"], "properties": { "type": { "type": "string", @@ -6801,12 +6297,7 @@ } } }, - "required": [ - "id", - "title", - "description", - "price" - ], + "required": ["id", "title", "description", "price"], "title": "Variant", "description": "A purchasable variant of a product with specific option selections.", "type": "object" @@ -6829,9 +6320,7 @@ "description": "Maximum compatible version (inclusive). When absent, no upper bound." } }, - "required": [ - "min" - ], + "required": ["min"], "additionalProperties": true }, "WarningCode": { @@ -6939,4 +6428,4 @@ "description": "Product/variant lookup by identifier. Supports batch retrieval (lookup_catalog) and single-product detail (get_product)." } ] -} \ No newline at end of file +} From 6c92f75d8f252146418c40f092c45a4870dbd9c1 Mon Sep 17 00:00:00 2001 From: Greg Smith Date: Thu, 10 Sep 2026 06:03:24 +0000 Subject: [PATCH 3/4] chore: remove obsolete compatibility aliases for unannotated types --- generate_models.sh | 34 +------------------------- src/ucp_sdk/models/__init__.py | 16 ------------ src/ucp_sdk/models/schemas/__init__.py | 18 +------------- 3 files changed, 2 insertions(+), 66 deletions(-) diff --git a/generate_models.sh b/generate_models.sh index 7a3d96b..9197fd6 100755 --- a/generate_models.sh +++ b/generate_models.sh @@ -89,24 +89,8 @@ cat << 'PY' > "$OUTPUT_DIR/__init__.py" """UCP schema models.""" -from .models import * # noqa: F403 from . import models - -# Compatibility aliases for unannotated request types & models -PaymentCreateRequest = models.Payment -PaymentUpdateRequest = models.Payment -PaymentCompleteRequest = models.Payment -OrderUpdateRequest = models.Order -AttributionCreateRequest = models.Attribution -AttributionUpdateRequest = models.Attribution -AttributionCompleteRequest = models.Attribution -BuyerCreateRequest = models.Buyer -BuyerUpdateRequest = models.Buyer -LocalityCreateRequest = models.Locality -LocalityUpdateRequest = models.Locality -ContextCreateRequest = models.Context -ContextUpdateRequest = models.Context -LineItemModel = models.LineItem +from .models import * # noqa: F403 __all__ = ["models"] PY @@ -131,22 +115,6 @@ cat << 'PY' > "src/ucp_sdk/models/__init__.py" from .schemas import models from .schemas.models import * # noqa: F403 -# Compatibility aliases for unannotated request types & models -PaymentCreateRequest = models.Payment -PaymentUpdateRequest = models.Payment -PaymentCompleteRequest = models.Payment -OrderUpdateRequest = models.Order -AttributionCreateRequest = models.Attribution -AttributionUpdateRequest = models.Attribution -AttributionCompleteRequest = models.Attribution -BuyerCreateRequest = models.Buyer -BuyerUpdateRequest = models.Buyer -LocalityCreateRequest = models.Locality -LocalityUpdateRequest = models.Locality -ContextCreateRequest = models.Context -ContextUpdateRequest = models.Context -LineItemModel = models.LineItem - __all__ = ["models"] PY diff --git a/src/ucp_sdk/models/__init__.py b/src/ucp_sdk/models/__init__.py index 31c82a0..1b6b1ff 100644 --- a/src/ucp_sdk/models/__init__.py +++ b/src/ucp_sdk/models/__init__.py @@ -17,20 +17,4 @@ from .schemas import models from .schemas.models import * # noqa: F403 -# Compatibility aliases for unannotated request types & models -PaymentCreateRequest = models.Payment -PaymentUpdateRequest = models.Payment -PaymentCompleteRequest = models.Payment -OrderUpdateRequest = models.Order -AttributionCreateRequest = models.Attribution -AttributionUpdateRequest = models.Attribution -AttributionCompleteRequest = models.Attribution -BuyerCreateRequest = models.Buyer -BuyerUpdateRequest = models.Buyer -LocalityCreateRequest = models.Locality -LocalityUpdateRequest = models.Locality -ContextCreateRequest = models.Context -ContextUpdateRequest = models.Context -LineItemModel = models.LineItem - __all__ = ["models"] diff --git a/src/ucp_sdk/models/schemas/__init__.py b/src/ucp_sdk/models/schemas/__init__.py index 13fa6b8..f9b7a3b 100644 --- a/src/ucp_sdk/models/schemas/__init__.py +++ b/src/ucp_sdk/models/schemas/__init__.py @@ -14,23 +14,7 @@ """UCP schema models.""" -from .models import * # noqa: F403 from . import models - -# Compatibility aliases for unannotated request types & models -PaymentCreateRequest = models.Payment -PaymentUpdateRequest = models.Payment -PaymentCompleteRequest = models.Payment -OrderUpdateRequest = models.Order -AttributionCreateRequest = models.Attribution -AttributionUpdateRequest = models.Attribution -AttributionCompleteRequest = models.Attribution -BuyerCreateRequest = models.Buyer -BuyerUpdateRequest = models.Buyer -LocalityCreateRequest = models.Locality -LocalityUpdateRequest = models.Locality -ContextCreateRequest = models.Context -ContextUpdateRequest = models.Context -LineItemModel = models.LineItem +from .models import * # noqa: F403 __all__ = ["models"] From fabf4f02dddbceda8d3b20829d5dbc644dc63c10 Mon Sep 17 00:00:00 2001 From: Greg Smith Date: Thu, 10 Sep 2026 06:27:05 +0000 Subject: [PATCH 4/4] feat(models): modular domain structure and complete elimination of legacy postprocessors - Generate consolidated models directly from OpenAPI 3.1 specification (shopping.openapi.json) - Synthesize modular domain packages (shopping/, common/types/, profile.py, etc.) to preserve consumer ergonomics and backwards compatibility - Delete postprocess_models.py (-1,965 LOC) and 227 legacy zombie schema files (-15,530 LOC) - Replace legacy test_codegen_pipeline.py with comprehensive, unskipped unit tests (tests/test_models.py) verifying polymorphic deserialization, directional requests, and validation invariants - Net reduction: ~10,350 lines of code --- generate_models.sh | 470 +++- postprocess_models.py | 2130 --------------- src/ucp_sdk/models/schemas/capability.py | 185 +- src/ucp_sdk/models/schemas/common/__init__.py | 18 +- .../models/schemas/common/identity_linking.py | 115 - .../models/schemas/common/location_lookup.py | 101 - .../models/schemas/common/location_search.py | 85 - src/ucp_sdk/models/schemas/common/loyalty.py | 297 --- .../schemas/common/payment_ap2_mandate.py | 146 -- .../schemas/common/payment_authentication.py | 101 - .../schemas/common/payment_split_payments.py | 80 - .../models/schemas/common/payment_terms.py | 99 - .../models/schemas/common/types/__init__.py | 32 +- .../models/schemas/common/types/actions.py | 55 +- .../schemas/common/types/amenity_type.py | 46 - .../models/schemas/common/types/amount.py | 31 +- .../types/available_payment_instrument.py | 41 - .../models/schemas/common/types/binding.py | 41 - .../types/business_split_payments_config.py | 48 - .../schemas/common/types/card_credential.py | 71 - .../common/types/card_payment_instrument.py | 100 +- .../common/types/constraint_expression.py | 101 - .../models/schemas/common/types/context.py | 85 +- .../common/types/context_create_request.py | 75 - .../common/types/context_update_request.py | 75 - .../models/schemas/common/types/daily_hour.py | 49 - .../common/types/daily_hour_create_request.py | 33 - .../common/types/daily_hour_update_request.py | 33 - .../schemas/common/types/description.py | 53 +- .../models/schemas/common/types/error_code.py | 48 - .../common/types/error_code_create_request.py | 48 - .../common/types/error_code_update_request.py | 48 - .../schemas/common/types/error_response.py | 45 +- .../schemas/common/types/exception_hour.py | 47 - .../types/exception_hour_create_request.py | 33 - .../types/exception_hour_update_request.py | 33 - .../models/schemas/common/types/geo.py | 39 - .../common/types/geo_create_request.py | 39 - .../common/types/geo_update_request.py | 39 - .../models/schemas/common/types/info_code.py | 44 - .../common/types/info_code_create_request.py | 44 - .../common/types/info_code_update_request.py | 44 - .../schemas/common/types/instrument_group.py | 43 - .../models/schemas/common/types/link.py | 39 - .../models/schemas/common/types/locality.py | 42 +- .../common/types/locality_create_request.py | 43 - .../common/types/locality_update_request.py | 43 - .../models/schemas/common/types/location.py | 69 - .../common/types/location_create_request.py | 47 - .../schemas/common/types/location_distance.py | 41 - .../schemas/common/types/location_filter.py | 90 - .../schemas/common/types/location_serves.py | 97 - .../types/location_serves_create_request.py | 97 - .../types/location_serves_update_request.py | 97 - .../schemas/common/types/location_summary.py | 45 - .../types/location_summary_create_request.py | 35 - .../types/location_summary_update_request.py | 35 - .../common/types/location_update_request.py | 47 - .../models/schemas/common/types/measure.py | 37 - .../common/types/measure_create_request.py | 37 - .../common/types/measure_update_request.py | 37 - .../models/schemas/common/types/media.py | 51 - .../models/schemas/common/types/message.py | 38 +- .../common/types/message_create_request.py | 43 - .../schemas/common/types/message_error.py | 57 - .../types/message_error_create_request.py | 57 - .../types/message_error_update_request.py | 57 - .../schemas/common/types/message_info.py | 48 - .../types/message_info_create_request.py | 48 - .../types/message_info_update_request.py | 48 - .../common/types/message_update_request.py | 43 - .../schemas/common/types/message_warning.py | 60 - .../types/message_warning_create_request.py | 60 - .../types/message_warning_update_request.py | 60 - .../common/types/network_token_credential.py | 67 - .../models/schemas/common/types/pagination.py | 91 - .../schemas/common/types/pan_credential.py | 59 - .../models/schemas/common/types/payment.py | 38 +- .../common/types/payment_complete_request.py | 40 - .../common/types/payment_create_request.py | 39 - .../common/types/payment_credential.py | 35 - .../payment_credential_complete_request.py | 35 - .../payment_credential_create_request.py | 35 - .../payment_credential_update_request.py | 35 - .../schemas/common/types/payment_identity.py | 35 - .../common/types/payment_instrument.py | 79 +- .../payment_instrument_complete_request.py | 78 - .../payment_instrument_create_request.py | 74 - .../payment_instrument_update_request.py | 74 - .../schemas/common/types/payment_schedule.py | 54 - .../schemas/common/types/payment_term.py | 50 - .../common/types/payment_update_request.py | 39 - .../models/schemas/common/types/policy.py | 49 +- .../schemas/common/types/postal_address.py | 62 +- .../types/postal_address_complete_request.py | 63 - .../types/postal_address_create_request.py | 63 - .../types/postal_address_update_request.py | 63 - .../models/schemas/common/types/price.py | 41 - .../schemas/common/types/price_filter.py | 41 - .../schemas/common/types/price_range.py | 41 - .../schemas/common/types/quantity_unit.py | 37 - .../types/quantity_unit_create_request.py | 37 - .../types/quantity_unit_update_request.py | 37 - .../common/types/request_constraints.py | 72 - .../common/types/reverse_domain_name.py | 48 - .../reverse_domain_name_create_request.py | 48 - .../reverse_domain_name_update_request.py | 48 - .../models/schemas/common/types/signals.py | 53 +- .../common/types/signals_complete_request.py | 54 - .../common/types/signals_create_request.py | 54 - .../common/types/signals_update_request.py | 54 - .../schemas/common/types/signed_amount.py | 40 - .../types/signed_amount_create_request.py | 40 - .../types/signed_amount_update_request.py | 40 - .../schemas/common/types/time_interval.py | 55 - .../types/time_interval_create_request.py | 31 - .../types/time_interval_update_request.py | 31 - .../schemas/common/types/token_credential.py | 41 - .../models/schemas/common/types/total.py | 83 - .../common/types/total_create_request.py | 31 - .../common/types/total_update_request.py | 31 - .../models/schemas/common/types/totals.py | 190 +- .../common/types/totals_create_request.py | 108 - .../common/types/totals_update_request.py | 108 - .../models/schemas/common/types/unit.py | 78 +- .../common/types/unit_create_request.py | 79 - .../common/types/unit_update_request.py | 79 - .../schemas/common/types/warning_code.py | 45 - .../types/warning_code_create_request.py | 45 - .../types/warning_code_update_request.py | 45 - src/ucp_sdk/models/schemas/payment_handler.py | 182 +- src/ucp_sdk/models/schemas/profile.py | 197 +- src/ucp_sdk/models/schemas/service.py | 584 +---- .../models/schemas/shopping/__init__.py | 21 +- .../models/schemas/shopping/buyer_consent.py | 145 -- src/ucp_sdk/models/schemas/shopping/cart.py | 116 +- .../schemas/shopping/cart_create_request.py | 69 - .../schemas/shopping/cart_update_request.py | 69 - .../models/schemas/shopping/catalog_lookup.py | 191 -- .../models/schemas/shopping/catalog_search.py | 80 - .../models/schemas/shopping/checkout.py | 120 +- .../shopping/checkout_complete_request.py | 39 - .../shopping/checkout_create_request.py | 56 - .../shopping/checkout_update_request.py | 56 - .../models/schemas/shopping/discount.py | 140 - .../models/schemas/shopping/fulfillment.py | 313 +-- src/ucp_sdk/models/schemas/shopping/order.py | 128 +- .../schemas/shopping/order_create_request.py | 116 - .../schemas/shopping/order_update_request.py | 116 - .../models/schemas/shopping/permalink.py | 52 - .../models/schemas/shopping/types/__init__.py | 29 +- .../schemas/shopping/types/adjustment.py | 82 - .../types/adjustment_create_request.py | 81 - .../types/adjustment_update_request.py | 81 - .../schemas/shopping/types/attribution.py | 25 +- .../types/attribution_complete_request.py | 28 - .../types/attribution_create_request.py | 28 - .../types/attribution_update_request.py | 28 - .../schemas/shopping/types/availability.py | 39 - .../types/business_fulfillment_config.py | 48 +- .../models/schemas/shopping/types/buyer.py | 42 +- .../shopping/types/buyer_create_request.py | 43 - .../shopping/types/buyer_update_request.py | 43 - .../models/schemas/shopping/types/category.py | 39 - .../shopping/types/detail_option_value.py | 41 - .../schemas/shopping/types/expectation.py | 71 - .../types/expectation_create_request.py | 71 - .../types/expectation_update_request.py | 71 - .../schemas/shopping/types/fulfillment.py | 43 - .../types/fulfillment_available_method.py | 47 - ...illment_available_method_create_request.py | 31 - ...illment_available_method_update_request.py | 31 - .../types/fulfillment_create_request.py | 40 - .../shopping/types/fulfillment_destination.py | 54 +- .../fulfillment_destination_create_request.py | 39 - .../types/fulfillment_destination_filter.py | 37 - .../fulfillment_destination_update_request.py | 39 - .../shopping/types/fulfillment_event.py | 77 - .../types/fulfillment_event_create_request.py | 77 - .../types/fulfillment_event_update_request.py | 77 - .../shopping/types/fulfillment_group.py | 49 - .../types/fulfillment_group_create_request.py | 35 - .../types/fulfillment_group_update_request.py | 39 - .../shopping/types/fulfillment_method.py | 136 +- .../fulfillment_method_create_request.py | 48 - .../fulfillment_method_update_request.py | 56 - .../shopping/types/fulfillment_option.py | 50 - .../shopping/types/fulfillment_option_base.py | 45 - .../fulfillment_option_base_create_request.py | 31 - .../fulfillment_option_base_update_request.py | 31 - .../fulfillment_option_create_request.py | 35 - .../fulfillment_option_update_request.py | 35 - .../types/fulfillment_update_request.py | 40 - .../shopping/types/input_correlation.py | 39 - .../models/schemas/shopping/types/item.py | 64 +- .../shopping/types/item_create_request.py | 39 - .../shopping/types/item_update_request.py | 39 - .../schemas/shopping/types/line_item.py | 57 +- .../types/line_item_create_request.py | 38 - .../types/line_item_update_request.py | 43 - .../shopping/types/location_destination.py | 39 - .../location_destination_create_request.py | 35 - .../location_destination_update_request.py | 35 - .../shopping/types/location_summary.py | 15 + .../schemas/shopping/types/option_value.py | 48 +- .../shopping/types/order_confirmation.py | 43 - .../schemas/shopping/types/order_line_item.py | 77 +- .../types/order_line_item_create_request.py | 78 - .../types/order_line_item_update_request.py | 78 - .../types/platform_fulfillment_config.py | 35 - .../models/schemas/shopping/types/product.py | 95 +- .../schemas/shopping/types/product_option.py | 41 - .../models/schemas/shopping/types/rating.py | 47 - .../schemas/shopping/types/search_filters.py | 38 - .../schemas/shopping/types/selected_option.py | 43 - .../shopping/types/shipping_destination.py | 43 - .../shipping_destination_create_request.py | 45 - .../shipping_destination_update_request.py | 45 - .../schemas/shopping/types/unit_price.py | 74 - .../models/schemas/shopping/types/variant.py | 151 +- .../models/schemas/transports/__init__.py | 17 - .../models/schemas/transports/a2a_message.py | 133 - .../schemas/transports/embedded_config.py | 41 - .../schemas/transports/embedded_message.py | 109 - .../models/schemas/transports/jsonrpc.py | 121 - .../schemas/transports/mcp_tool_call.py | 157 -- src/ucp_sdk/models/schemas/ucp.py | 405 +-- .../models/schemas/ucp_create_request.py | 409 --- .../models/schemas/ucp_update_request.py | 409 --- tests/test_codegen_pipeline.py | 2295 ----------------- tests/test_models.py | 339 +++ 231 files changed, 1127 insertions(+), 19794 deletions(-) delete mode 100644 postprocess_models.py delete mode 100644 src/ucp_sdk/models/schemas/common/identity_linking.py delete mode 100644 src/ucp_sdk/models/schemas/common/location_lookup.py delete mode 100644 src/ucp_sdk/models/schemas/common/location_search.py delete mode 100644 src/ucp_sdk/models/schemas/common/loyalty.py delete mode 100644 src/ucp_sdk/models/schemas/common/payment_ap2_mandate.py delete mode 100644 src/ucp_sdk/models/schemas/common/payment_authentication.py delete mode 100644 src/ucp_sdk/models/schemas/common/payment_split_payments.py delete mode 100644 src/ucp_sdk/models/schemas/common/payment_terms.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/amenity_type.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/available_payment_instrument.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/binding.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/business_split_payments_config.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/card_credential.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/constraint_expression.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/context_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/context_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/daily_hour.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/daily_hour_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/daily_hour_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/error_code.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/error_code_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/error_code_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/exception_hour.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/exception_hour_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/exception_hour_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/geo.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/geo_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/geo_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/info_code.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/info_code_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/info_code_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/instrument_group.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/link.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/locality_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/locality_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/location.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/location_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/location_distance.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/location_filter.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/location_serves.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/location_serves_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/location_serves_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/location_summary.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/location_summary_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/location_summary_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/location_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/measure.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/measure_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/measure_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/media.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/message_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/message_error.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/message_error_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/message_error_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/message_info.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/message_info_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/message_info_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/message_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/message_warning.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/message_warning_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/message_warning_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/network_token_credential.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/pagination.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/pan_credential.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/payment_complete_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/payment_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/payment_credential.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/payment_credential_complete_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/payment_credential_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/payment_credential_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/payment_identity.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/payment_instrument_complete_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/payment_instrument_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/payment_instrument_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/payment_schedule.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/payment_term.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/payment_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/postal_address_complete_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/postal_address_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/postal_address_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/price.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/price_filter.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/price_range.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/quantity_unit.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/quantity_unit_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/quantity_unit_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/request_constraints.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/reverse_domain_name.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/reverse_domain_name_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/reverse_domain_name_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/signals_complete_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/signals_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/signals_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/signed_amount.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/signed_amount_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/signed_amount_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/time_interval.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/time_interval_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/time_interval_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/token_credential.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/total.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/total_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/total_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/totals_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/totals_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/unit_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/unit_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/warning_code.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/warning_code_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/common/types/warning_code_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/buyer_consent.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/cart_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/cart_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/catalog_lookup.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/catalog_search.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/checkout_complete_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/checkout_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/checkout_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/discount.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/order_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/order_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/permalink.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/adjustment.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/adjustment_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/adjustment_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/attribution_complete_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/attribution_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/attribution_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/availability.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/buyer_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/buyer_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/category.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/detail_option_value.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/expectation.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/expectation_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/expectation_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/fulfillment.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/fulfillment_available_method.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/fulfillment_available_method_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/fulfillment_available_method_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/fulfillment_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/fulfillment_destination_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/fulfillment_destination_filter.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/fulfillment_destination_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/fulfillment_event.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/fulfillment_event_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/fulfillment_event_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/fulfillment_group.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/fulfillment_group_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/fulfillment_group_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/fulfillment_method_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/fulfillment_method_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/fulfillment_option.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/fulfillment_option_base.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/fulfillment_option_base_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/fulfillment_option_base_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/fulfillment_option_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/fulfillment_option_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/fulfillment_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/input_correlation.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/item_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/item_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/line_item_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/line_item_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/location_destination.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/location_destination_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/location_destination_update_request.py create mode 100644 src/ucp_sdk/models/schemas/shopping/types/location_summary.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/order_confirmation.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/order_line_item_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/order_line_item_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/platform_fulfillment_config.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/product_option.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/rating.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/search_filters.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/selected_option.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/shipping_destination.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/shipping_destination_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/shipping_destination_update_request.py delete mode 100644 src/ucp_sdk/models/schemas/shopping/types/unit_price.py delete mode 100644 src/ucp_sdk/models/schemas/transports/__init__.py delete mode 100644 src/ucp_sdk/models/schemas/transports/a2a_message.py delete mode 100644 src/ucp_sdk/models/schemas/transports/embedded_config.py delete mode 100644 src/ucp_sdk/models/schemas/transports/embedded_message.py delete mode 100644 src/ucp_sdk/models/schemas/transports/jsonrpc.py delete mode 100644 src/ucp_sdk/models/schemas/transports/mcp_tool_call.py delete mode 100644 src/ucp_sdk/models/schemas/ucp_create_request.py delete mode 100644 src/ucp_sdk/models/schemas/ucp_update_request.py delete mode 100644 tests/test_codegen_pipeline.py create mode 100644 tests/test_models.py diff --git a/generate_models.sh b/generate_models.sh index 9197fd6..67ab14c 100755 --- a/generate_models.sh +++ b/generate_models.sh @@ -71,7 +71,473 @@ uv run \ --use-one-literal-as-default \ --use-default -# Ensure package re-exports and request type compatibility aliases +# Generate modular domain structure re-exporting from models.py +python3 - <<'PY' +from pathlib import Path + +BASE = Path("src/ucp_sdk/models/schemas") + +MODULES = { + # Shopping domain + "shopping/checkout.py": """\"\"\"Shopping checkout models.\"\"\" +from __future__ import annotations + +from ..models import ( + Checkout, + CheckoutCompleteRequest, + CheckoutCreateRequest, + CheckoutUpdateRequest, +) + +__all__ = [ + "Checkout", + "CheckoutCompleteRequest", + "CheckoutCreateRequest", + "CheckoutUpdateRequest", +] +""", + "shopping/cart.py": """\"\"\"Shopping cart models.\"\"\" +from __future__ import annotations + +from ..models import ( + Cart, + CartCreateRequest, + CartUpdateRequest, +) + +__all__ = [ + "Cart", + "CartCreateRequest", + "CartUpdateRequest", +] +""", + "shopping/order.py": """\"\"\"Shopping order models.\"\"\" +from __future__ import annotations + +from ..models import ( + Order, + OrderCreateRequest, + OrderUpdateRequest, +) + +__all__ = [ + "Order", + "OrderCreateRequest", + "OrderUpdateRequest", +] +""", + "shopping/fulfillment.py": """\"\"\"Shopping fulfillment models.\"\"\" +from __future__ import annotations + +from ..models import ( + Fulfillment, + FulfillmentCreateRequest, + FulfillmentUpdateRequest, +) + +__all__ = [ + "Fulfillment", + "FulfillmentCreateRequest", + "FulfillmentUpdateRequest", +] +""", + "shopping/types/fulfillment_destination.py": """\"\"\"Fulfillment destination models.\"\"\" +from __future__ import annotations + +from ...models import ( + FulfillmentDestination, + LocationDestination, + LocationDestinationCreateRequest, + LocationDestinationUpdateRequest, + ShippingDestination, + ShippingDestinationCreateRequest, + ShippingDestinationUpdateRequest, +) + +__all__ = [ + "FulfillmentDestination", + "LocationDestination", + "LocationDestinationCreateRequest", + "LocationDestinationUpdateRequest", + "ShippingDestination", + "ShippingDestinationCreateRequest", + "ShippingDestinationUpdateRequest", +] +""", + "shopping/types/fulfillment_method.py": """\"\"\"Fulfillment method models.\"\"\" +from __future__ import annotations + +from ...models import ( + FulfillmentMethod, + FulfillmentMethodCreateRequest, + FulfillmentMethodUpdateRequest, +) + +__all__ = [ + "FulfillmentMethod", + "FulfillmentMethodCreateRequest", + "FulfillmentMethodUpdateRequest", +] +""", + "shopping/types/business_fulfillment_config.py": """\"\"\"Business fulfillment configuration.\"\"\" +from __future__ import annotations + +from ...models import BusinessFulfillmentConfig + +__all__ = ["BusinessFulfillmentConfig"] +""", + "shopping/types/line_item.py": """\"\"\"Line item models.\"\"\" +from __future__ import annotations + +from ...models import ( + LineItem, + LineItemCreateRequest, + LineItemModel, + LineItemUpdateRequest, +) + +__all__ = [ + "LineItem", + "LineItemCreateRequest", + "LineItemModel", + "LineItemUpdateRequest", +] +""", + "shopping/types/item.py": """\"\"\"Item models.\"\"\" +from __future__ import annotations + +from ...models import ( + Item, + ItemCreateRequest, + ItemUpdateRequest, +) + +__all__ = [ + "Item", + "ItemCreateRequest", + "ItemUpdateRequest", +] +""", + "shopping/types/buyer.py": """\"\"\"Buyer models.\"\"\" +from __future__ import annotations + +from ...models import Buyer + +__all__ = ["Buyer"] +""", + "shopping/types/attribution.py": """\"\"\"Attribution models.\"\"\" +from __future__ import annotations + +from ...models import Attribution + +__all__ = ["Attribution"] +""", + "shopping/types/order_line_item.py": """\"\"\"Order line item models.\"\"\" +from __future__ import annotations + +from ...models import OrderLineItem + +__all__ = ["OrderLineItem"] +""", + "shopping/types/product.py": """\"\"\"Product models.\"\"\" +from __future__ import annotations + +from ...models import DetailProduct, FulfillmentProduct, Product + +__all__ = ["Product", "DetailProduct", "FulfillmentProduct"] +""", + "shopping/types/variant.py": """\"\"\"Variant models.\"\"\" +from __future__ import annotations + +from ...models import FulfillmentVariant, LookupVariant, Variant + +__all__ = ["Variant", "LookupVariant", "FulfillmentVariant"] +""", + "shopping/types/option_value.py": """\"\"\"Option value models.\"\"\" +from __future__ import annotations + +from ...models import DetailOptionValue, OptionValue, ProductOption, SelectedOption + +__all__ = ["OptionValue", "SelectedOption", "ProductOption", "DetailOptionValue"] +""", + "shopping/types/location_summary.py": """\"\"\"Location summary models.\"\"\" +from __future__ import annotations + +from ...models import ( + LocationSummary, + LocationSummaryCreateRequest, + LocationSummaryUpdateRequest, +) + +__all__ = [ + "LocationSummary", + "LocationSummaryCreateRequest", + "LocationSummaryUpdateRequest", +] +""", + "shopping/__init__.py": """\"\"\"Shopping models.\"\"\" +from .checkout import * # noqa: F403 +from .cart import * # noqa: F403 +from .order import * # noqa: F403 +from .fulfillment import * # noqa: F403 +""", + "shopping/types/__init__.py": """\"\"\"Shopping auxiliary types.\"\"\" +from .fulfillment_destination import * # noqa: F403 +from .fulfillment_method import * # noqa: F403 +from .business_fulfillment_config import * # noqa: F403 +from .line_item import * # noqa: F403 +from .item import * # noqa: F403 +from .buyer import * # noqa: F403 +from .attribution import * # noqa: F403 +from .order_line_item import * # noqa: F403 +from .product import * # noqa: F403 +from .variant import * # noqa: F403 +from .option_value import * # noqa: F403 +from .location_summary import * # noqa: F403 +""", + + # Common domain + "common/types/totals.py": """\"\"\"Totals models.\"\"\" +from __future__ import annotations + +from ...models import ( + Total, + TotalCreateRequest, + Totals, + TotalsCreateRequest, + TotalsUpdateRequest, + TotalUpdateRequest, +) + +__all__ = [ + "Totals", + "TotalsCreateRequest", + "TotalsUpdateRequest", + "Total", + "TotalCreateRequest", + "TotalUpdateRequest", +] +""", + "common/types/amount.py": """\"\"\"Amount and price models.\"\"\" +from __future__ import annotations + +from ...models import Amount, Price, SignedAmount + +__all__ = ["Amount", "Price", "SignedAmount"] +""", + "common/types/unit.py": """\"\"\"Unit models.\"\"\" +from __future__ import annotations + +from ...models import QuantityUnit, Unit, UnitPrice + +__all__ = ["Unit", "QuantityUnit", "UnitPrice"] +""", + "common/types/signals.py": """\"\"\"Signals models.\"\"\" +from __future__ import annotations + +from ...models import Signals + +__all__ = ["Signals"] +""", + "common/types/description.py": """\"\"\"Description models.\"\"\" +from __future__ import annotations + +from ...models import Description + +__all__ = ["Description"] +""", + "common/types/card_payment_instrument.py": """\"\"\"Card payment instrument models.\"\"\" +from __future__ import annotations + +from ...models import AvailablePaymentInstrument, PaymentInstrument + +CardPaymentInstrument = PaymentInstrument + +__all__ = ["CardPaymentInstrument", "AvailablePaymentInstrument", "PaymentInstrument"] +""", + "common/types/payment_instrument.py": """\"\"\"Payment instrument models.\"\"\" +from __future__ import annotations + +from ...models import AvailablePaymentInstrument, PaymentInstrument, SelectedPaymentInstrument + +__all__ = ["PaymentInstrument", "AvailablePaymentInstrument", "SelectedPaymentInstrument"] +""", + "common/types/error_response.py": """\"\"\"Error response models.\"\"\" +from __future__ import annotations + +from ...models import Error, ErrorCode, ErrorResponse + +__all__ = ["ErrorResponse", "Error", "ErrorCode"] +""", + "common/types/postal_address.py": """\"\"\"Postal address models.\"\"\" +from __future__ import annotations + +from ...models import PostalAddress + +__all__ = ["PostalAddress"] +""", + "common/types/payment.py": """\"\"\"Payment models.\"\"\" +from __future__ import annotations + +from ...models import Payment, PaymentCredential + +__all__ = ["Payment", "PaymentCredential"] +""", + "common/types/message.py": """\"\"\"Message models.\"\"\" +from __future__ import annotations + +from ...models import Message, MessageError, MessageInfo, MessageWarning + +__all__ = ["Message", "MessageError", "MessageInfo", "MessageWarning"] +""", + "common/types/locality.py": """\"\"\"Locality models.\"\"\" +from __future__ import annotations + +from ...models import Locality + +__all__ = ["Locality"] +""", + "common/types/context.py": """\"\"\"Context models.\"\"\" +from __future__ import annotations + +from ...models import Context + +__all__ = ["Context"] +""", + "common/types/policy.py": """\"\"\"Policy models.\"\"\" +from __future__ import annotations + +from ...models import Policy + +__all__ = ["Policy"] +""", + "common/types/actions.py": """\"\"\"Actions models.\"\"\" +from __future__ import annotations + +from ...models import Actions + +__all__ = ["Actions"] +""", + "common/__init__.py": """\"\"\"Common models.\"\"\" +from .types import * # noqa: F403 +""", + "common/types/__init__.py": """\"\"\"Common types.\"\"\" +from .totals import * # noqa: F403 +from .amount import * # noqa: F403 +from .unit import * # noqa: F403 +from .signals import * # noqa: F403 +from .description import * # noqa: F403 +from .card_payment_instrument import * # noqa: F403 +from .payment_instrument import * # noqa: F403 +from .error_response import * # noqa: F403 +from .postal_address import * # noqa: F403 +from .payment import * # noqa: F403 +from .message import * # noqa: F403 +from .locality import * # noqa: F403 +from .context import * # noqa: F403 +from .policy import * # noqa: F403 +from .actions import * # noqa: F403 +""", + + # Root schemas + "profile.py": """\"\"\"Profile models.\"\"\" +from __future__ import annotations + +from .models import JwkPublicKey, Profile, ProfileBase + +__all__ = ["Profile", "ProfileBase", "JwkPublicKey"] +""", + "capability.py": """\"\"\"Capability models.\"\"\" +from __future__ import annotations + +from .models import ( + CapabilityBase, + CapabilityBusinessSchema, + CapabilityPlatformSchema, + CapabilityResponseSchema, +) + +Base = CapabilityBase +__all__ = [ + "CapabilityBase", + "CapabilityBusinessSchema", + "CapabilityPlatformSchema", + "CapabilityResponseSchema", + "Base", +] +""", + "service.py": """\"\"\"Service models.\"\"\" +from __future__ import annotations + +from .models import ( + ServiceBase, + ServiceBusinessSchema, + ServicePlatformSchema, + ServiceResponseSchema, +) + +Base = ServiceBase +__all__ = [ + "ServiceBase", + "ServiceBusinessSchema", + "ServicePlatformSchema", + "ServiceResponseSchema", + "Base", +] +""", + "payment_handler.py": """\"\"\"Payment handler models.\"\"\" +from __future__ import annotations + +from .models import ( + PaymentHandlerBase, + PaymentHandlerBusinessSchema, + PaymentHandlerPlatformSchema, + PaymentHandlerResponseSchema, +) + +Base = PaymentHandlerBase +__all__ = [ + "PaymentHandlerBase", + "PaymentHandlerBusinessSchema", + "PaymentHandlerPlatformSchema", + "PaymentHandlerResponseSchema", + "Base", +] +""", + "ucp.py": """\"\"\"UCP core models.\"\"\" +from __future__ import annotations + +from .models import ( + Ucp, + UcpBase, + UcpBusinessSchema, + UcpCreateRequest, + UcpEntity, + UcpPlatformSchema, + UcpUpdateRequest, +) + +__all__ = [ + "Ucp", + "UcpBase", + "UcpBusinessSchema", + "UcpPlatformSchema", + "UcpEntity", + "UcpCreateRequest", + "UcpUpdateRequest", +] +""", +} + +for rel_path, code in MODULES.items(): + target = BASE / rel_path + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(code, encoding="utf-8") + +print("Generated modular domain schemas successfully.") +PY + +# Package root re-exports cat << 'PY' > "$OUTPUT_DIR/__init__.py" # Copyright 2026 UCP Authors # @@ -133,4 +599,4 @@ echo "Formatting generated models..." uv run ruff format "$OUTPUT_DIR" uv run ruff check --fix "$OUTPUT_DIR" -echo "Done. Models generated in $MODELS_FILE" +echo "Done. Models generated in $MODELS_FILE and modular packages." diff --git a/postprocess_models.py b/postprocess_models.py deleted file mode 100644 index 110a783..0000000 --- a/postprocess_models.py +++ /dev/null @@ -1,2130 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Post-generation fixes for constraints datamodel-code-generator ignores. - -Ten constraint families are handled: - -* ``minProperties`` / ``maxProperties`` on an object schema WITH declared - properties are dropped by the generator: every field is optional, so an - empty instance (or, for ``maxProperties``, an over-full one) passes - validation in violation of the schema. ``minProperties`` support (issue - #49, PR #55) never grew a ``maxProperties`` counterpart, so - ``location_serves.json``'s ``maxProperties: 1`` ("the Platform MUST - supply exactly one target form") went unenforced even though its sibling - ``minProperties: 1`` on the same schema was already caught. (Either bound - on a free-form object property is already handled natively — the - generator maps it to ``Field(min_length=..., max_length=...)`` on the - dict field.) The script scans the preprocessed schemas for root-level - ``minProperties``/``maxProperties`` constraints and injects a - ``model_validator(mode="after")`` into the matching generated classes, - one validator per bound so both can coexist on the same class. JSON - Schema counts the keys present on the object, so the validator counts - provided fields (``model_fields_set``) unioned with extra keys - (``model_extra``) — an explicit null is a present key, and unknown keys on - ``extra="allow"`` models count too. - -* ``contains`` / ``minContains`` / ``maxContains`` on an array schema is likewise - dropped by the generator: ``totals.json`` requires *exactly one* ``subtotal`` - *and exactly one* ``total`` entry, but the generated ``Totals`` is a bare - ``list[Total]`` alias, so an empty array (or one missing either required entry, - or with duplicates) validates in violation of the schema. An array root is - emitted as a ``TypeAliasType`` wrapping ``Annotated[list[...], ...]`` rather - than a ``BaseModel`` subclass, so ``model_validator`` cannot apply; this script - instead injects a module-level counting function, threaded into the alias - metadata as a ``pydantic.AfterValidator``. Every predicate is derived from - ``contains.properties..const`` — nothing is hard-coded — and one function - enforces *all* of a schema's contains bounds. - - The pristine (pre-preprocessing) schemas are read for this: ``totals.json`` - carries its two containment rules as two ``allOf`` branches, and - ``preprocess_schemas.py`` merges ``allOf`` into the root, where a JSON node can - hold only one ``contains`` — so the second (``total``) would be lost if the - preprocessed output were scanned. generate_models.sh snapshots the originals to - ``ucp/raw_schemas`` before preprocessing for exactly this reason. The bound is - applied to the base model and to its generated request variants (linked by file - stem), and travels wherever the alias is reused as a field type. - -* ``propertyNames`` on an object WITH named ``properties`` is not enforced. Such - a schema is emitted as a ``BaseModel(extra="allow")`` with the named fields, so - unknown (extra) keys are accepted without being checked against the declared - key pattern (``signals.json`` requires reverse-domain keys, yet a malformed - extra key validates). The script scans for objects that declare - ``propertyNames`` AND carry named ``properties`` and injects a - ``model_validator(mode="after")`` that matches every ``model_extra`` key against - the pattern. The pattern is read from the source schema (inline or via ``$ref`` - to e.g. ``reverse_domain_name.json``), never duplicated here. An object with - ``propertyNames`` but *no* named properties is emitted as a ``dict[KeyType, V]`` - whose key type already carries the pattern, so it is out of scope. - -* ``uniqueItems`` on an array is dropped entirely by the generator, so a list - field accepts duplicate entries in violation of the schema. The script - collects the names of array properties declared with ``uniqueItems`` and - injects a ``field_validator(mode="after")`` into each generated class that - declares a matching list field. - -* Simple conditional ``required`` constraints are dropped: pagination requires - ``cursor`` when ``has_next_page`` is true, but the generated response model - always treats it as optional. The script accepts only an unambiguous single - required discriminator using ``const``/``enum`` and a ``then.required`` list, - then injects a ``model_validator(mode="after")``. More complex conditions are - skipped rather than approximated. These rules are commonly carried as - ``allOf`` branches with no sibling ``properties`` of their own (every - conditional rule in ``profile.json``'s ``jwk_public_key`` is this shape), so - the scan validates them against the enclosing object's property set, the - same threading ``find_conditional_bounds`` (below) already used. A branch's - own documentation ``title`` (some carry one purely as rule prose, e.g. "EC - keys carry crv, x, y") is never adopted as the enclosing class name; see - ``_is_bare_conditional_branch``. - -* Conditional numeric bounds, and conditional exact-value (``const``) pins, - are dropped for the same reason: ``total.json`` requires a ``discount`` - amount to be negative and a ``tax`` amount to be non-negative via if/then - branches, and ``unit.json`` pins ``scale`` to exactly 0 when ``unit`` is - ``C62``, but the generated ``Total``/``Unit`` carry no validator, so a - positive discount or a nonzero C62 scale both validate. These rules are - carried as ``allOf`` branches, which have no sibling ``properties`` of - their own, so the scan validates them against the enclosing object's - property set. A rule whose fields were stripped by request-variant - projection is inapplicable rather than malformed and is skipped silently. - As with conditional required rules above, a branch's own documentation - ``title`` is never adopted as the enclosing class name. - -* A discriminator retyping an array PROPERTY's items to a schema file - different from the property's own base ``$ref`` is dropped entirely, a - third if/then shape distinct from the required-fields and numeric-bounds - families above. ``fulfillment_method.json``'s ``destinations`` stays typed - to the base ``FulfillmentDestination`` regardless of ``type``, even though - a `shipping` method's destinations are really ``ShippingDestination`` - (postal address fields, `type` const `shipping_address`) and a `pickup` - method's are really ``LocationDestination`` (`type` const - `business_location`) — so a `shipping` method can currently list a - destination typed `business_location` and it validates. Pydantic has no - clean way to retype a field's item type from a source-text splice, so - this is enforced with a runtime check instead of a static type change: - each item is checked against the referenced schema's own (root-level, - post-merge) required keys and const-pinned properties — an approximation, - not a full re-derivation of the retyped type (a schema the retyped file - itself ``allOf``-references, e.g. ``postal_address.json``, is not - inspected). - -* ``dependentRequired`` on an object is dropped entirely by the generator. For - example, ``time_interval.json`` allows an empty fragment but requires ``opens`` - and ``closes`` to appear together; the generated ``TimeInterval`` instead - accepts either field alone. The script scans root object schemas for valid - dependent-field maps and injects a ``model_validator(mode="after")`` that uses - property presence (``model_fields_set`` plus extra keys), not value truthiness. - A rule naming a field absent from a projected generated class is skipped rather - than approximated. - -* ``additionalProperties: false`` on an object schema with named properties is - normally overridden by the generator's ``--extra-fields=allow`` flag. The - script detects schemas with ``additionalProperties: false`` and flips their - generated ``model_config`` to ``extra="forbid"`` while preserving - ``extra="allow"`` on sibling models in the same module. - -Runs from generate_models.sh between generation and formatting; idempotent. -""" - -import json -import re -import sys -from pathlib import Path - -SCHEMA_DIR = Path("ucp/source/schemas") -# Pristine schemas snapshotted by generate_models.sh before preprocessing. -# Array contains bounds are read from here, not SCHEMA_DIR, because -# preprocessing merges allOf and can drop a second contains keyword. -RAW_SCHEMA_DIR = Path("ucp/raw_schemas") -OUTPUT_DIR = Path("src/ucp_sdk/models/schemas") - -_MARKER = "_enforce_min_properties" - -_VALIDATOR_TEMPLATE = ''' - @model_validator(mode="after") - def {marker}(self): - """JSON Schema minProperties: require at least {minimum} - provided {properties_noun}.""" - provided = self.model_fields_set | set(self.model_extra or {{}}) - if len(provided) < {minimum}: - raise ValueError( - "At least {minimum} {properties_noun} must be provided " - "(schema minProperties={minimum})" - ) - return self -''' - -_MAX_MARKER = "_enforce_max_properties" - -_MAX_VALIDATOR_TEMPLATE = ''' - @model_validator(mode="after") - def {marker}(self): - """JSON Schema maxProperties: allow at most {maximum} - provided {properties_noun}.""" - provided = self.model_fields_set | set(self.model_extra or {{}}) - if len(provided) > {maximum}: - raise ValueError( - "At most {maximum} {properties_noun} may be provided " - "(schema maxProperties={maximum})" - ) - return self -''' - -_PROPNAMES_MARKER = "_enforce_property_names" - -_PROPNAMES_VALIDATOR_TEMPLATE = ''' - @model_validator(mode="after") - def {marker}(self): - """JSON Schema propertyNames: every extra key must match the - declared reverse-domain pattern (schema propertyNames).""" - pattern = {pattern!r} - for key in self.model_extra or {{}}: - if re.fullmatch(pattern, key) is None: - raise ValueError( - f"Property name {{key!r}} does not match the schema " - f"propertyNames pattern {{pattern}}" - ) - return self -''' - -_DEPENDENT_REQUIRED_MARKER = "_enforce_dependent_required" - -_DEPENDENT_REQUIRED_TEMPLATE = ''' - @model_validator(mode="after") - def {marker}(self): - """JSON Schema dependentRequired: enforce dependent fields.""" - rules = {rules!r} - provided = self.model_fields_set | set(self.model_extra or {{}}) - for field, required_fields in rules.items(): - if field not in provided: - continue - for required in required_fields: - if required not in provided: - raise ValueError( - f"Field {{required!r}} is required when {{field!r}} " - "is provided (schema dependentRequired)" - ) - return self -''' - -_UNIQUE_MARKER = "_enforce_unique_items" - -_CONDITIONAL_REQUIRED_MARKER = "_enforce_conditional_required" - -_CONDITIONAL_REQUIRED_TEMPLATE = ''' - @model_validator(mode="after") - def {marker}(self): - """JSON Schema if/then: enforce conditionally required fields.""" - rules = {rules!r} - for rule in rules: - if getattr(self, rule["discriminator"], None) not in rule["values"]: - continue - for field in rule["required"]: - if field not in self.model_fields_set: - raise ValueError( - f"Field {{field!r}} is required by a schema condition" - ) - return self -''' - -_CONDITIONAL_BOUNDS_MARKER = "_enforce_conditional_bounds" - -# Returned when a rule is well-formed but names fields absent from the class it -# would apply to — distinct from None, which means the shape is unsupported and -# warrants a warning. -_RULE_NOT_APPLICABLE = object() - -# Keyword -> (comparison rendered in the message, python operator name). The -# operator is applied as "value limit" and a true result is a violation. -# "const" pins the field to an exact value (e.g. unit.json: scale must be -# exactly 0 when unit is C62) rather than bounding a range; it reuses the -# same "value limit -> violation" shape with not-equal as the operator. -_BOUND_KEYWORDS = { - "minimum": (">=", "lt"), - "maximum": ("<=", "gt"), - "exclusiveMinimum": (">", "le"), - "exclusiveMaximum": ("<", "ge"), - "const": ("==", "ne"), -} - -_CONDITIONAL_BOUNDS_TEMPLATE = ''' - @model_validator(mode="after") - def {marker}(self): - """JSON Schema if/then: enforce conditional numeric bounds.""" - rules = {rules!r} - checks = {checks!r} - for rule in rules: - actual = getattr(self, rule["discriminator"], None) - if actual not in rule["values"]: - continue - for field, bounds in rule["bounds"].items(): - value = getattr(self, field, None) - if value is None: - continue - for keyword, limit in bounds.items(): - symbol, op_name = checks[keyword] - if getattr(operator, op_name)(value, limit): - raise ValueError( - f"Field {{field!r}} must be {{symbol}} {{limit}} " - f"when {{rule['discriminator']}} is {{actual!r}}" - ) - return self -''' - -_RETYPE_MARKER = "_enforce_conditional_item_retyping" - -_RETYPE_TEMPLATE = ''' - @model_validator(mode="after") - def {marker}(self): - """JSON Schema if/then: approximate a discriminator's array-item - retyping to a different referenced schema, via that schema's own - required keys and const-pinned fields.""" - rules = {rules!r} - for rule in rules: - actual = getattr(self, rule["discriminator"], None) - if actual not in rule["values"]: - continue - for _item in getattr(self, rule["field"], None) or []: - _provided = ( - set(_item.keys()) - if isinstance(_item, dict) - else _item.model_fields_set | set(_item.model_extra or {{}}) - ) - for _required in rule["required"]: - if _required not in _provided: - raise ValueError( - f"Field {{_required!r}} is required for " - f"{{rule['field']}} items when " - f"{{rule['discriminator']}} is {{actual!r}}" - ) - for _const_field, _const_value in rule["consts"].items(): - _actual_value = ( - _item.get(_const_field) - if isinstance(_item, dict) - else getattr(_item, _const_field, None) - ) - if _actual_value != _const_value: - raise ValueError( - f"Field {{_const_field!r}} must equal " - f"{{_const_value!r}} for {{rule['field']}} items " - f"when {{rule['discriminator']}} is {{actual!r}}" - ) - return self -''' - -_UNIQUE_VALIDATOR_TEMPLATE = ''' - @field_validator("{field}", mode="after") - def {marker}_{field}(cls, value): # noqa: N805 - """JSON Schema uniqueItems: reject duplicate entries.""" - if value is None: - return value - seen = [] - for item in value: - if item in seen: - raise ValueError( - "Items must be unique (schema uniqueItems=true)" - ) - seen.append(item) - return value -''' - - -def find_root_min_properties(schema_dir): - """Map schema title -> minProperties for root-level object constraints.""" - found = {} - for path in sorted(Path(schema_dir).rglob("*.json")): - try: - schema = json.loads(path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): - continue - if not isinstance(schema, dict): - continue - minimum = schema.get("minProperties") - if not minimum or not schema.get("properties"): - continue - title = schema.get("title") - if not title: - sys.stderr.write( - f" ! {path}: root minProperties but no title; " - "cannot map to a class\n" - ) - continue - found[_alias_name(title)] = minimum - return found - - -def find_root_max_properties(schema_dir): - """Map schema title -> maxProperties for root-level object constraints. - - Symmetric twin of find_root_min_properties (see #49/#55, which added - minProperties support but never a maxProperties counterpart): - maxProperties on an object schema WITH declared properties is dropped by - the generator the same way minProperties is, so - location_serves.json's maxProperties: 1 ("the Platform MUST supply - exactly one target form") was silently unenforced. As with the min - side, maxProperties on a free-form object property (no named - properties) is already handled natively by the generator - (Field(max_length=...) on the dict field), so it is out of scope here. - """ - found = {} - for path in sorted(Path(schema_dir).rglob("*.json")): - try: - schema = json.loads(path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): - continue - if not isinstance(schema, dict): - continue - maximum = schema.get("maxProperties") - if not isinstance(maximum, int) or not schema.get("properties"): - continue - title = schema.get("title") - if not title: - sys.stderr.write( - f" ! {path}: root maxProperties but no title; " - "cannot map to a class\n" - ) - continue - found[_alias_name(title)] = maximum - return found - - -def _ensure_pydantic_import(source, symbol): - """Add ``symbol`` to the ``from pydantic import`` line if absent.""" - if re.search( - rf"^from pydantic import .*\b{re.escape(symbol)}\b", source, re.M - ): - return source - return re.sub( - r"^(from pydantic import [^\n]+)$", - lambda m: f"{m.group(1)}, {symbol}", - source, - count=1, - flags=re.M, - ) - - -def _ensure_stdlib_import(source, statement): - """Add a top-level ``import`` statement if absent. - - Inserted right after ``from __future__ import annotations`` so ruff's - isort pass (run later in the pipeline) settles it into the stdlib group. - """ - if re.search(rf"^{re.escape(statement)}$", source, re.M): - return source - return re.sub( - r"^(from __future__ import annotations\n)", - lambda m: f"{m.group(1)}\n{statement}\n", - source, - count=1, - flags=re.M, - ) - - -def _resolve_property_names_pattern(prop_names, schema_path): - """Return the key pattern a ``propertyNames`` node enforces, or ``None``. - - Reads an inline ``pattern`` directly, or follows a ``$ref`` to an external - schema file's root ``pattern`` (e.g. ``reverse_domain_name.json``) so the - pattern is never duplicated here — it always comes from the source schema. - Local ``#/...`` pointer refs are not resolved and are skipped with a - warning rather than guessed. - """ - if not isinstance(prop_names, dict): - return None - inline = prop_names.get("pattern") - if isinstance(inline, str): - return inline - ref = prop_names.get("$ref") - if not isinstance(ref, str): - return None - if ref.startswith("#"): - sys.stderr.write( - f" ! {schema_path}: propertyNames $ref '{ref}' is a local " - "pointer; pattern not resolved\n" - ) - return None - file_part = ref.split("#", 1)[0] - target = (Path(schema_path).parent / file_part).resolve() - try: - referenced = json.loads(target.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): - sys.stderr.write( - f" ! {schema_path}: propertyNames $ref '{ref}' could not be " - "loaded; pattern not resolved\n" - ) - return None - pattern = ( - referenced.get("pattern") if isinstance(referenced, dict) else None - ) - if not isinstance(pattern, str): - sys.stderr.write( - f" ! {schema_path}: propertyNames $ref '{ref}' target has no " - "root pattern; not resolved\n" - ) - return None - return pattern - - -def find_property_names_patterns(schema_dir): - """Map generated class name -> propertyNames pattern for extra-allow models. - - The gap this targets: an object schema that declares ``propertyNames`` AND - carries named ``properties`` is emitted by the generator as a - ``BaseModel(extra="allow")`` with those named fields, so unknown (extra) - keys are never pattern-checked. An object with ``propertyNames`` but *no* - named ``properties`` is emitted as a ``dict[KeyType, V]`` map whose key type - already carries the pattern (pydantic validates the keys), so it is out of - scope. The class is defined mechanically: has ``propertyNames`` (resolvable - to a pattern) AND non-empty ``properties`` AND a ``title`` to map to a class. - Nested titled objects are walked too, so the rule is general, not per-file. - """ - found = {} - - def walk(node, path_str): - if not isinstance(node, dict): - if isinstance(node, list): - for item in node: - walk(item, path_str) - return - props = node.get("properties") - if "propertyNames" in node and isinstance(props, dict) and props: - pattern = _resolve_property_names_pattern( - node["propertyNames"], path_str - ) - title = node.get("title") - if pattern is None: - pass - elif not title: - sys.stderr.write( - f" ! {path_str}: propertyNames on an extra-allow object " - "but no title; cannot map to a class\n" - ) - else: - # The injected validator uses re.fullmatch to mirror - # pydantic-core / ECMA-262 (JSON Schema's regex dialect) key - # semantics, which the sibling dict-map path already applies. - # That is exact for the ^...$-anchored patterns UCP uses. An - # unanchored pattern means JSON Schema unanchored-search - # semantics, where fullmatch would over-restrict; warn so a - # future schema does not silently get a stricter check. - if not (pattern.startswith("^") and pattern.endswith("$")): - sys.stderr.write( - f" ! {path_str}: propertyNames pattern {pattern!r} is " - "not ^/$-anchored; fullmatch enforcement may be " - "stricter than JSON Schema search semantics\n" - ) - found[_alias_name(title)] = pattern - for value in node.values(): - walk(value, path_str) - - for path in sorted(Path(schema_dir).rglob("*.json")): - try: - schema = json.loads(path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): - continue - walk(schema, str(path)) - return found - - -def inject_property_names(source, class_name, pattern): - """Inject the propertyNames key validator at the end of ``class_name``.""" - class_re = re.compile(rf"^class {re.escape(class_name)}\(", re.M) - match = class_re.search(source) - if not match: - return source - # The class body ends at the next top-level statement or EOF. - tail = re.compile(r"^\S", re.M) - end_match = tail.search(source, match.end()) - end = end_match.start() if end_match else len(source) - # Scope the idempotency guard to this class's own body, so a second - # target class in the same module is still patched. - if f"def {_PROPNAMES_MARKER}(" in source[match.start() : end]: - return source - method = _PROPNAMES_VALIDATOR_TEMPLATE.format( - marker=_PROPNAMES_MARKER, pattern=pattern - ) - body = source[:end].rstrip("\n") - rest = source[end:] - out = body + "\n" + method + ("\n" + rest if rest else "") - out = _ensure_pydantic_import(out, "model_validator") - return _ensure_stdlib_import(out, "import re") - - -def inject_min_properties(source, class_name, minimum): - """Inject the minProperties validator at the end of ``class_name``.""" - if f"def {_MARKER}(" in source: - return source - class_re = re.compile(rf"^class {re.escape(class_name)}\(", re.M) - match = class_re.search(source) - if not match: - return source - # The class body ends at the next top-level statement or EOF. - tail = re.compile(r"^\S", re.M) - end_match = tail.search(source, match.end()) - end = end_match.start() if end_match else len(source) - method = _VALIDATOR_TEMPLATE.format( - marker=_MARKER, - minimum=minimum, - properties_noun="property" if minimum == 1 else "properties", - ) - body = source[:end].rstrip("\n") - rest = source[end:] - out = body + "\n" + method + ("\n" + rest if rest else "") - return _ensure_pydantic_import(out, "model_validator") - - -def inject_max_properties(source, class_name, maximum): - """Inject the maxProperties validator at the end of ``class_name``. - - Symmetric twin of inject_min_properties; both validators can be - injected into the same class (location_serves.json declares both - minProperties: 1 and maxProperties: 1), each guarded by its own marker - so neither injection clobbers the other or re-runs on a second pass. - """ - if f"def {_MAX_MARKER}(" in source: - return source - class_re = re.compile(rf"^class {re.escape(class_name)}\(", re.M) - match = class_re.search(source) - if not match: - return source - # The class body ends at the next top-level statement or EOF. - tail = re.compile(r"^\S", re.M) - end_match = tail.search(source, match.end()) - end = end_match.start() if end_match else len(source) - method = _MAX_VALIDATOR_TEMPLATE.format( - marker=_MAX_MARKER, - maximum=maximum, - properties_noun="property" if maximum == 1 else "properties", - ) - body = source[:end].rstrip("\n") - rest = source[end:] - out = body + "\n" + method + ("\n" + rest if rest else "") - return _ensure_pydantic_import(out, "model_validator") - - -def _extract_contains_groups(schema, path=None): - """Collect every array ``contains`` group from a schema's root + allOf. - - Each group is ``{"pairs": [(field, const), ...], "min": int, - "max": int | None}``, derived from ``contains.properties..const`` - with its ``minContains`` / ``maxContains`` bounds. A ``contains`` keyword - may sit at the schema root or inside any ``allOf`` branch; each contributes - a group, so "exactly one subtotal and one total" yields two. The predicate - is read from the schema, never hard-coded. - """ - nodes = [schema] - if isinstance(schema.get("allOf"), list): - nodes.extend(n for n in schema["allOf"] if isinstance(n, dict)) - groups = [] - for node in nodes: - contains = node.get("contains") - if not isinstance(contains, dict): - continue - props = contains.get("properties") - pairs = [] - if isinstance(props, dict): - for field, spec in props.items(): - if isinstance(spec, dict) and "const" in spec: - pairs.append((field, spec["const"])) - if not pairs: - if path is not None: - sys.stderr.write( - f" ! {path}: contains predicate has no " - "properties.*.const; cannot derive a check\n" - ) - continue - # JSON Schema: minContains defaults to 1 when contains is present. - groups.append( - { - "pairs": pairs, - "min": node.get("minContains", 1), - "max": node.get("maxContains"), - } - ) - return groups - - -def find_array_contains_constraints(schema_dir): - """Map file stem -> ``{"title": str, "groups": [...]}`` for array schemas. - - Keyed by file stem (not title) so a base schema can be linked to its - generated request variants, whose stems extend it (``totals`` -> - ``totals_create_request``). Scanned against the *pristine* schemas - (``RAW_SCHEMA_DIR``); see the module docstring for why the preprocessed - output must not be used here. - """ - found = {} - for path in sorted(Path(schema_dir).rglob("*.json")): - try: - schema = json.loads(path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): - continue - if not isinstance(schema, dict): - continue - # ``contains`` only constrains arrays; skip anything else. - if schema.get("type") != "array" and "items" not in schema: - continue - groups = _extract_contains_groups(schema, path) - if not groups: - continue - item_condition = _extract_item_required_condition(schema) - title = schema.get("title") - if not title: - sys.stderr.write( - f" ! {path}: array contains constraint but no title; " - "cannot map to a model\n" - ) - continue - found[path.stem] = { - "title": title, - "groups": groups, - "item_condition": item_condition, - } - return found - - -def _extract_item_required_condition(schema): - """Read a simple array-item ``not.enum`` + ``then.required`` rule.""" - items = schema.get("items") - if not isinstance(items, dict): - return None - nodes = [items] - nodes.extend( - node for node in items.get("allOf", []) if isinstance(node, dict) - ) - for node in nodes: - condition = node.get("if") - consequence = node.get("then") - if not isinstance(condition, dict) or not isinstance(consequence, dict): - continue - props = condition.get("properties") - required = consequence.get("required") - if not isinstance(props, dict) or len(props) != 1: - continue - field, predicate = next(iter(props.items())) - if ( - not isinstance(predicate, dict) - or set(node) != {"if", "then"} - or set(condition) != {"properties", "required"} - or set(consequence) != {"required"} - ): - continue - excluded = predicate.get("not") - values = excluded.get("enum") if isinstance(excluded, dict) else None - if ( - condition.get("required") == [field] - and set(predicate) == {"not"} - and isinstance(excluded, dict) - and set(excluded) == {"enum"} - and isinstance(values, list) - and values - and all(isinstance(value, str) for value in values) - and isinstance(required, list) - and required - and all(isinstance(name, str) for name in required) - ): - return {"field": field, "excluded": values, "required": required} - return None - - -def _alias_name(title): - """Derive the generated alias name from a schema title (drop spaces).""" - return "".join(title.split()) - - -def _is_bare_conditional_branch(node): - """True when ``node`` is an if/then rule wrapper with no type of its own. - - UCP schemas sometimes give an allOf if/then branch its own human-readable - ``title`` purely as rule documentation (e.g. profile.json's jwk_public_key: - "EC keys carry crv, x, y", "P-256 pairs with ES256"). Such a branch has no - ``properties`` of its own -- it constrains the *enclosing* object -- so its - title never names a generated class. Adopting it as ``current_class_name`` - (the same code path real class-defining titles use) misattributes the - rule to a nonexistent class instead of the enclosing one. A node that - carries both an if/then rule AND its own ``properties`` is a genuine - titled type that happens to declare an inline conditional, and keeps - adopting its title as before. - """ - return ( - isinstance(node.get("if"), dict) - and isinstance(node.get("then"), dict) - and not isinstance(node.get("properties"), dict) - ) - - -def _to_camel_case(string): - """Convert a string (snake, kebab, space-separated) to CamelCase.""" - parts = re.split(r"[^a-zA-Z0-9]", string) - return "".join(p.capitalize() for p in parts if p) - - -def _snake_name(name): - """CamelCase alias -> snake_case suffix for a unique function name.""" - return re.sub(r"(? {maximum}:", - " raise ValueError(", - f' "Array must contain at most {maximum} {noun} "', - f' "matching {desc} (schema maxContains={maximum})"', - " )", - ] - if item_condition: - field = item_condition["field"] - lines += [ - f" _excluded = {item_condition['excluded']!r}", - " for _item in value:", - f" _actual = (_item.get({field!r}) if isinstance(_item, dict) ", - f" else getattr(_item, {field!r}, None))", - " if _actual in _excluded:", - " continue", - ] - for required in item_condition["required"]: - lines += [ - f" if isinstance(_item, dict) and {required!r} not in _item:", - f' raise ValueError("Field {required!r} is required for custom {field}")', - f" if not isinstance(_item, dict) and {required!r} not in _item.model_fields_set:", - f' raise ValueError("Field {required!r} is required for custom {field}")', - ] - lines.append(" return value") - return "\n".join(lines) + "\n" - - -def inject_array_contains(source, alias_name, groups, item_condition=None): - """Thread an ``AfterValidator`` into ``alias_name``'s alias metadata. - - Array roots are emitted as ``NAME = TypeAliasType("NAME", Annotated[...])``, - not a ``BaseModel`` subclass, so the constraint is enforced by inserting - ``AfterValidator()`` into the ``Annotated[...]`` metadata and defining - ```` just above the assignment. Idempotent via the function name. - """ - func_name = f"_enforce_contains_{_snake_name(alias_name)}" - if f"def {func_name}(" in source: - return source - assign_re = re.compile(rf"^{re.escape(alias_name)} = TypeAliasType\(", re.M) - match = assign_re.search(source) - if not match: - return source - ann_start = source.find("Annotated[", match.end()) - if ann_start == -1: - return source - # Bracket-match to the ``]`` that closes ``Annotated[``. - depth = 0 - close = None - for pos in range(ann_start + len("Annotated"), len(source)): - char = source[pos] - if char == "[": - depth += 1 - elif char == "]": - depth -= 1 - if depth == 0: - close = pos - break - if close is None: - return source - # Insert right after the last real token inside Annotated[...], not - # blindly right before the closing bracket. When the annotation is - # line-wrapped -- which ruff/black do once the item type reference is - # long enough to push the line past the wrap width, e.g. a - # request-variant $ref such as total_create_request.TotalCreateRequest - # replacing the shorter total.Total -- there is already a trailing - # comma just before the whitespace that precedes "]". Splicing before - # that whitespace would leave the existing trailing comma and our own - # leading comma separated by nothing but whitespace: two commas with no - # expression between them, a SyntaxError (see #34/#35). - scan = close - 1 - while scan >= 0 and source[scan] in " \t\n": - scan -= 1 - if scan >= 0 and source[scan] == ",": - insert_at = scan + 1 - addition = f" AfterValidator({func_name})," - else: - insert_at = scan + 1 - addition = f", AfterValidator({func_name})" - out = source[:insert_at] + addition + source[insert_at:] - func_src = _build_contains_function(func_name, groups, item_condition) - insert_at = assign_re.search(out).start() - out = out[:insert_at] + func_src + "\n\n" + out[insert_at:] - return _ensure_pydantic_import(out, "AfterValidator") - - -def find_conditional_required(schema_dir): - """Map generated class names to simple if/then required rules.""" - rules_by_class = {} - - def describe(node, properties): - if not isinstance(node, dict) or set(node) != {"if", "then"}: - return None - condition = node["if"] - consequence = node["then"] - if ( - not isinstance(condition, dict) - or set(condition) != {"properties", "required"} - or not isinstance(consequence, dict) - or set(consequence) != {"required"} - ): - return None - condition_props = condition["properties"] - condition_required = condition["required"] - consequence_required = consequence["required"] - if ( - not isinstance(condition_props, dict) - or len(condition_props) != 1 - or not isinstance(condition_required, list) - or len(condition_required) != 1 - or not isinstance(consequence_required, list) - or not consequence_required - ): - return None - discriminator, predicate = next(iter(condition_props.items())) - if condition_required != [discriminator] or not isinstance( - predicate, dict - ): - return None - if set(predicate) == {"const"}: - values = [predicate["const"]] - elif ( - set(predicate) == {"enum"} - and isinstance(predicate["enum"], list) - and predicate["enum"] - ): - values = predicate["enum"] - else: - return None - if ( - discriminator not in properties - or any( - not isinstance(name, str) or name not in properties - for name in consequence_required - ) - or any( - not isinstance(value, (str, int, float, bool)) - for value in values - ) - ): - return None - return { - "discriminator": discriminator, - "values": values, - "required": sorted(consequence_required), - } - - def walk(node, current_class_name, path_str, enclosing_properties=None): - if not isinstance(node, dict): - return - if isinstance( - node.get("title"), str - ) and not _is_bare_conditional_branch(node): - current_class_name = _alias_name(node["title"]) - properties = node.get("properties") - # An if/then pair carried as an allOf branch has no sibling - # properties of its own (every JWK required-field rule is exactly - # this shape): the object it constrains is the enclosing schema, so - # its property set is what the rule must be validated against. This - # mirrors find_conditional_bounds's existing enclosing_properties - # threading. - scope = ( - properties if isinstance(properties, dict) else enclosing_properties - ) - then = node.get("then") - is_required_rule = isinstance(then, dict) and "required" in then - if isinstance(scope, dict) and is_required_rule: - if "else" in node: - rule = None - else: - rule = describe( - {key: node[key] for key in ("if", "then") if key in node}, - scope, - ) - if rule is None: - sys.stderr.write( - f" ! {path_str}: unsupported conditional required rule; skipped\n" - ) - elif current_class_name is not None: - rules_by_class.setdefault(current_class_name, []).append(rule) - if isinstance(properties, dict): - for name, prop in properties.items(): - walk(prop, _to_camel_case(name), path_str) - defs = node.get("$defs") - if isinstance(defs, dict): - for def_name, def_node in defs.items(): - walk(def_node, _to_camel_case(def_name), path_str) - for key in ("allOf", "anyOf", "oneOf"): - if isinstance(node.get(key), list): - for item in node[key]: - walk(item, current_class_name, path_str, scope) - - for path in sorted(Path(schema_dir).rglob("*.json")): - try: - schema = json.loads(path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): - continue - if not isinstance(schema, dict): - continue - root_title = schema.get("title") - initial_class = ( - _alias_name(root_title) if root_title else _to_camel_case(path.stem) - ) - walk(schema, initial_class, str(path)) - return rules_by_class - - -def inject_conditional_required(source, class_name, rules): - """Inject simple conditional-required checks into one generated class.""" - class_re = re.compile(rf"^class {re.escape(class_name)}\(", re.M) - match = class_re.search(source) - if not match: - return source - tail = re.compile(r"^\S", re.M) - end_match = tail.search(source, match.end()) - end = end_match.start() if end_match else len(source) - if f"def {_CONDITIONAL_REQUIRED_MARKER}(" in source[match.start() : end]: - return source - method = _CONDITIONAL_REQUIRED_TEMPLATE.format( - marker=_CONDITIONAL_REQUIRED_MARKER, - rules=rules, - ) - body = source[:end].rstrip("\n") - rest = source[end:] - out = body + "\n" + method + ("\n" + rest if rest else "") - return _ensure_pydantic_import(out, "model_validator") - - -def find_conditional_bounds(schema_dir): - """Map generated class names to if/then numeric-bound rules. - - Complements find_conditional_required, which only handles a ``then`` that - adds required fields. A ``then`` that instead narrows a numeric range is - dropped by datamodel-code-generator, so the constraint would otherwise be - absent from the generated model entirely. - """ - rules_by_class = {} - - def describe(node, properties): - if not isinstance(node, dict) or set(node) != {"if", "then"}: - return None - condition = node["if"] - consequence = node["then"] - if ( - not isinstance(condition, dict) - or set(condition) != {"properties", "required"} - or not isinstance(consequence, dict) - or set(consequence) != {"properties"} - ): - return None - condition_props = condition["properties"] - condition_required = condition["required"] - consequence_props = consequence["properties"] - if ( - not isinstance(condition_props, dict) - or len(condition_props) != 1 - or not isinstance(condition_required, list) - or len(condition_required) != 1 - or not isinstance(consequence_props, dict) - or not consequence_props - ): - return None - discriminator, predicate = next(iter(condition_props.items())) - if condition_required != [discriminator] or not isinstance( - predicate, dict - ): - return None - if set(predicate) == {"const"}: - values = [predicate["const"]] - elif ( - set(predicate) == {"enum"} - and isinstance(predicate["enum"], list) - and predicate["enum"] - ): - values = predicate["enum"] - else: - return None - if any( - not isinstance(value, (str, int, float, bool)) for value in values - ): - return None - bounds = {} - for name, constraint in consequence_props.items(): - if ( - not isinstance(name, str) - or not isinstance(constraint, dict) - or not constraint - or set(constraint) - set(_BOUND_KEYWORDS) - ): - return None - # "const" pins an exact value and may legitimately be a string - # (unit.json's C62 pin is an int; JWK's algorithm pins are - # strings), so it is checked against the same scalar types - # already accepted for the discriminator's own const/enum - # values above. The numeric bound keywords keep their existing, - # narrower int/float (non-bool) requirement. - for keyword, limit in constraint.items(): - if keyword == "const": - if not isinstance(limit, (str, int, float, bool)): - return None - elif not isinstance(limit, (int, float)) or isinstance( - limit, bool - ): - return None - bounds[name] = dict(constraint) - # A request variant strips the fields a platform must not send, so a - # rule naming one is inapplicable to that class rather than malformed. - if discriminator not in properties or any( - name not in properties for name in bounds - ): - return _RULE_NOT_APPLICABLE - return { - "discriminator": discriminator, - "values": values, - "bounds": bounds, - } - - def walk(node, current_class_name, path_str, enclosing_properties=None): - if not isinstance(node, dict): - return - if isinstance( - node.get("title"), str - ) and not _is_bare_conditional_branch(node): - current_class_name = _alias_name(node["title"]) - properties = node.get("properties") - # An if/then pair carried as an allOf branch has no sibling properties: - # the object it constrains is the enclosing schema, so its property set - # is what the rule must be validated against. - scope = ( - properties if isinstance(properties, dict) else enclosing_properties - ) - then = node.get("then") - is_bounds_rule = ( - isinstance(then, dict) - and "properties" in then - and "required" not in then - ) - if isinstance(scope, dict) and is_bounds_rule: - rule = ( - None - if "else" in node - else describe( - {key: node[key] for key in ("if", "then") if key in node}, - scope, - ) - ) - if rule is None: - sys.stderr.write( - f" ! {path_str}: unsupported conditional bounds rule; skipped\n" - ) - elif rule is not _RULE_NOT_APPLICABLE and current_class_name: - rules_by_class.setdefault(current_class_name, []).append(rule) - if isinstance(properties, dict): - for name, prop in properties.items(): - walk(prop, _to_camel_case(name), path_str) - defs = node.get("$defs") - if isinstance(defs, dict): - for def_name, def_node in defs.items(): - walk(def_node, _to_camel_case(def_name), path_str) - for key in ("allOf", "anyOf", "oneOf"): - if isinstance(node.get(key), list): - for item in node[key]: - walk(item, current_class_name, path_str, scope) - - for path in sorted(Path(schema_dir).rglob("*.json")): - try: - schema = json.loads(path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): - continue - if not isinstance(schema, dict): - continue - root_title = schema.get("title") - initial_class = ( - _alias_name(root_title) if root_title else _to_camel_case(path.stem) - ) - walk(schema, initial_class, str(path)) - return rules_by_class - - -def inject_conditional_bounds(source, class_name, rules): - """Inject conditional numeric-bound checks into one generated class.""" - class_re = re.compile(rf"^class {re.escape(class_name)}\(", re.M) - match = class_re.search(source) - if not match: - return source - tail = re.compile(r"^\S", re.M) - end_match = tail.search(source, match.end()) - end = end_match.start() if end_match else len(source) - if f"def {_CONDITIONAL_BOUNDS_MARKER}(" in source[match.start() : end]: - return source - method = _CONDITIONAL_BOUNDS_TEMPLATE.format( - marker=_CONDITIONAL_BOUNDS_MARKER, - rules=rules, - checks=_BOUND_KEYWORDS, - ) - body = source[:end].rstrip("\n") - rest = source[end:] - out = body + "\n" + method + ("\n" + rest if rest else "") - out = _ensure_stdlib_import(out, "import operator") - return _ensure_pydantic_import(out, "model_validator") - - -def _resolve_referenced_shape(ref, schema_path): - """Load ``ref`` (relative to ``schema_path``) and return its own - (root-level, post-merge) required keys and const-pinned properties. - - Returns ``None`` if the file cannot be loaded. Deliberately shallow: it - reads only the referenced schema's own ``required``/``properties``, not - anything it in turn ``allOf``-references (e.g. shipping_destination.json - ``allOf``-refs postal_address.json, whose fields are not inspected) -- - an approximation, not a full re-derivation of the retyped shape. - """ - file_part = ref.split("#", 1)[0] - if not file_part: - return None - target_path = (Path(schema_path).parent / file_part).resolve() - try: - referenced = json.loads(target_path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): - sys.stderr.write( - f" ! {schema_path}: retyped $ref {ref!r} could not be loaded; " - "rule skipped\n" - ) - return None - if not isinstance(referenced, dict): - return None - required = sorted( - name for name in referenced.get("required", []) if isinstance(name, str) - ) - consts = { - name: prop["const"] - for name, prop in (referenced.get("properties") or {}).items() - if isinstance(prop, dict) and "const" in prop - } - return {"required": required, "consts": consts} - - -def _is_ref_array(node): - """True when ``node`` is an array property typed via ``items.$ref``.""" - return ( - isinstance(node, dict) - and node.get("type") == "array" - and isinstance(node.get("items"), dict) - and isinstance(node["items"].get("$ref"), str) - ) - - -def _describe_retyping_branch(branch, properties, schema_path): - """Describe one allOf if/then branch that retypes an array property's - items to a schema file different from the property's own base ``$ref``. - - Mechanical and narrow by design, mirroring the other conditional - scanners in this module: a single-key ``const``/``enum`` discriminator - naming a property present on the enclosing object, and a ``then`` that - narrows exactly one array property (also present on the enclosing - object) to a different ``items.$ref``. Anything else -- multiple - discriminators, a non-array or non-$ref field, a `then` naming a field - absent from the enclosing object (a request variant that omits it, as - fulfillment_method_create_request.json does for ``destinations``) -- - returns ``None`` silently: those are either a different rule shape - (left to find_conditional_required/find_conditional_bounds, which scan - the same branches) or legitimately inapplicable, not malformed. - """ - if not isinstance(branch, dict) or set(branch) != {"if", "then"}: - return None - condition = branch["if"] - consequence = branch["then"] - if ( - not isinstance(condition, dict) - or set(condition) != {"properties", "required"} - or not isinstance(consequence, dict) - or set(consequence) != {"properties"} - ): - return None - condition_props = condition["properties"] - condition_required = condition["required"] - if ( - not isinstance(condition_props, dict) - or len(condition_props) != 1 - or not isinstance(condition_required, list) - or len(condition_required) != 1 - ): - return None - discriminator, predicate = next(iter(condition_props.items())) - if condition_required != [discriminator] or not isinstance(predicate, dict): - return None - if set(predicate) == {"const"}: - values = [predicate["const"]] - elif ( - set(predicate) == {"enum"} - and isinstance(predicate["enum"], list) - and predicate["enum"] - ): - values = predicate["enum"] - else: - return None - if discriminator not in properties or any( - not isinstance(value, (str, int, float, bool)) for value in values - ): - return None - consequence_props = consequence["properties"] - if not isinstance(consequence_props, dict) or len(consequence_props) != 1: - return None - field, field_schema = next(iter(consequence_props.items())) - if field not in properties or not _is_ref_array(field_schema): - return None - base_field_schema = properties[field] - if not _is_ref_array(base_field_schema): - return None - base_ref = base_field_schema["items"]["$ref"] - new_ref = field_schema["items"]["$ref"] - if new_ref == base_ref: - return None - target = _resolve_referenced_shape(new_ref, schema_path) - if target is None: - return None - return { - "discriminator": discriminator, - "values": values, - "field": field, - "required": target["required"], - "consts": target["consts"], - } - - -def find_conditional_array_retyping(schema_dir): - """Map generated class names to array-item retyping rules. - - Complements find_conditional_required/find_conditional_bounds, which - only handle a ``then`` that adds required fields or narrows a numeric - range. A ``then`` that instead retypes an array PROPERTY's items to a - schema file different from the property's own base ``$ref`` is a third - shape the generator drops entirely: fulfillment_method.json's - ``destinations`` stays typed to the base FulfillmentDestination - regardless of ``type``, even though a `shipping` method's destinations - are really ShippingDestination (postal address fields, `type` const - `shipping_address`) and a `pickup` method's are really - LocationDestination (`type` const `business_location`). Pydantic has no - clean way to retype a field's item type from a source-text splice, so - this is enforced with a runtime check instead of a static type change: - each item is checked against the referenced schema's own required keys - and const-pinned fields (see _resolve_referenced_shape), an - approximation rather than a full re-derivation of the retyped type. - """ - rules_by_class = {} - - def walk(node, current_class_name, schema_path): - if not isinstance(node, dict): - return - if isinstance(node.get("title"), str): - current_class_name = _alias_name(node["title"]) - properties = node.get("properties") - allof = node.get("allOf") - if isinstance(properties, dict) and isinstance(allof, list): - for branch in allof: - rule = _describe_retyping_branch( - branch, properties, schema_path - ) - if rule is not None and current_class_name is not None: - rules_by_class.setdefault(current_class_name, []).append( - rule - ) - if isinstance(properties, dict): - for name, prop in properties.items(): - walk(prop, _to_camel_case(name), schema_path) - defs = node.get("$defs") - if isinstance(defs, dict): - for def_name, def_node in defs.items(): - walk(def_node, _to_camel_case(def_name), schema_path) - - for path in sorted(Path(schema_dir).rglob("*.json")): - try: - schema = json.loads(path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): - continue - if not isinstance(schema, dict): - continue - root_title = schema.get("title") - initial_class = ( - _alias_name(root_title) if root_title else _to_camel_case(path.stem) - ) - walk(schema, initial_class, path) - return rules_by_class - - -def inject_conditional_array_retyping(source, class_name, rules): - """Inject array-item retyping checks into one generated class.""" - class_re = re.compile(rf"^class {re.escape(class_name)}\(", re.M) - match = class_re.search(source) - if not match: - return source - tail = re.compile(r"^\S", re.M) - end_match = tail.search(source, match.end()) - end = end_match.start() if end_match else len(source) - if f"def {_RETYPE_MARKER}(" in source[match.start() : end]: - return source - method = _RETYPE_TEMPLATE.format(marker=_RETYPE_MARKER, rules=rules) - body = source[:end].rstrip("\n") - rest = source[end:] - out = body + "\n" + method + ("\n" + rest if rest else "") - return _ensure_pydantic_import(out, "model_validator") - - -def find_root_dependent_required(schema_dir): - """Map generated class names to root-level dependentRequired rules. - - Only complete rules over declared properties are returned. Request variants - can project either side out; those rules are inapplicable to that generated - class and are skipped by ``inject_dependent_required``. - """ - found = {} - for path in sorted(Path(schema_dir).rglob("*.json")): - try: - schema = json.loads(path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): - continue - if not isinstance(schema, dict): - continue - properties = schema.get("properties") - rules = schema.get("dependentRequired") - title = schema.get("title") - if ( - not isinstance(properties, dict) - or not properties - or not isinstance(rules, dict) - or not rules - ): - continue - normalized = {} - malformed = False - for field, required in rules.items(): - if ( - not isinstance(field, str) - or not isinstance(required, list) - or not required - or not all(isinstance(name, str) for name in required) - ): - malformed = True - break - # Request variants may project either side out. Such a rule no - # longer applies to that generated class and is skipped silently. - if field in properties and all( - name in properties for name in required - ): - normalized[field] = required - if malformed: - sys.stderr.write( - f" ! {path}: unsupported dependentRequired rule; skipped\n" - ) - continue - if not normalized: - continue - if not isinstance(title, str) or not title: - sys.stderr.write( - f" ! {path}: root dependentRequired but no title; " - "cannot map to a class\n" - ) - continue - found[_alias_name(title)] = normalized - return found - - -def inject_dependent_required(source, class_name, rules): - """Inject root dependentRequired checks into one generated class.""" - class_re = re.compile(rf"^class {re.escape(class_name)}\(", re.M) - match = class_re.search(source) - if not match: - return source - tail = re.compile(r"^\S", re.M) - end_match = tail.search(source, match.end()) - end = end_match.start() if end_match else len(source) - class_body = source[match.start() : end] - if f"def {_DEPENDENT_REQUIRED_MARKER}(" in class_body: - return source - declared = { - field.group(1) - for field in re.finditer(r"^ (\w+): [^\n]+", class_body, re.M) - } - applicable = { - field: required - for field, required in rules.items() - if field in declared and all(name in declared for name in required) - } - if not applicable: - return source - method = _DEPENDENT_REQUIRED_TEMPLATE.format( - marker=_DEPENDENT_REQUIRED_MARKER, - rules=applicable, - ) - body = source[:end].rstrip("\n") - rest = source[end:] - out = body + "\n" + method + ("\n" + rest if rest else "") - return _ensure_pydantic_import(out, "model_validator") - - -def find_unique_items_fields(schema_dir): - """Map generated class names to fields carrying ``uniqueItems``. - - A schema node needs a title so its constraint can be associated with a - generated class. Untitled nodes are resolved using their property path. - """ - fields_by_class = {} - - def walk(node, current_class_name, path_str): - if not isinstance(node, dict): - return - - if isinstance(node.get("title"), str): - current_class_name = _alias_name(node["title"]) - - props = node.get("properties") - if isinstance(props, dict): - for name, prop in props.items(): - if not isinstance(prop, dict): - continue - - if prop.get("uniqueItems") is True and ( - prop.get("type") == "array" or "items" in prop - ): - if current_class_name is None: - sys.stderr.write( - f" ! {path_str}: uniqueItems field '{name}' " - "belongs to an untitled object; cannot map to a class\n" - ) - continue - fields_by_class.setdefault(current_class_name, set()).add( - name - ) - - # Recurse into properties - next_class_name = ( - _to_camel_case(name) if current_class_name else None - ) - walk(prop, next_class_name, path_str) - - # Recurse into $defs - defs = node.get("$defs") - if isinstance(defs, dict): - for def_name, def_node in defs.items(): - walk(def_node, _to_camel_case(def_name), path_str) - - # Recurse into combinators (allOf, anyOf, oneOf) - for key in ("allOf", "anyOf", "oneOf"): - if isinstance(node.get(key), list): - for item in node[key]: - walk(item, current_class_name, path_str) - - for path in sorted(Path(schema_dir).rglob("*.json")): - try: - schema = json.loads(path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): - continue - if not isinstance(schema, dict): - continue - - root_title = schema.get("title") - initial_class = ( - _alias_name(root_title) if root_title else _to_camel_case(path.stem) - ) - walk(schema, initial_class, str(path)) - - return fields_by_class - - -def inject_unique_items(source, unique_fields_by_class): - """Inject uniqueness validators for list fields declared ``uniqueItems``. - - A validator is added only when both the generated class name and list - field name match the scoped schema constraints. - """ - if not unique_fields_by_class: - return source - class_re = re.compile(r"^class (\w+)\(", re.M) - matches = list(class_re.finditer(source)) - if not matches: - return source - new_source = source - patched = False - # Process from the last class to the first so earlier insert offsets - # (computed against the original source) stay valid as text is appended. - for match in reversed(matches): - unique_fields = unique_fields_by_class.get(match.group(1), set()) - if not unique_fields: - continue - body_start = match.end() - tail = re.compile(r"^\S", re.M) - end_match = tail.search(source, body_start) - body_end = end_match.start() if end_match else len(source) - body = source[body_start:body_end] - targets = [] - for field_match in re.finditer( - r"^ (\w+): [^\n]*\blist\[", body, re.M - ): - field = field_match.group(1) - marker = f"def {_UNIQUE_MARKER}_{field}(" - if field in unique_fields and marker not in body: - targets.append(field) - if not targets: - continue - methods = "".join( - _UNIQUE_VALIDATOR_TEMPLATE.format( - marker=_UNIQUE_MARKER, field=field - ) - for field in targets - ) - prefix = new_source[:body_end].rstrip("\n") - suffix = new_source[body_end:] - new_source = prefix + methods + ("\n" + suffix if suffix else "") - patched = True - if patched: - new_source = _ensure_pydantic_import(new_source, "field_validator") - return new_source - - -def _patch_min_properties(): - """Inject minProperties validators; return (patched_count, exit_code).""" - constraints = find_root_min_properties(SCHEMA_DIR) - if not constraints: - sys.stdout.write( - "postprocess: no root-level minProperties constraints found\n" - ) - return 0, 0 - patched = 0 - for title, minimum in sorted(constraints.items()): - hits = [] - for path in sorted(OUTPUT_DIR.rglob("*.py")): - source = path.read_text(encoding="utf-8") - if not re.search(rf"^class {re.escape(title)}\(", source, re.M): - continue - updated = inject_min_properties(source, title, minimum) - if updated != source: - path.write_text(updated, encoding="utf-8") - patched += 1 - hits.append(path) - label = ", ".join(str(h) for h in hits) or "NO GENERATED CLASS FOUND" - sys.stdout.write(f" minProperties={minimum} on '{title}' -> {label}\n") - if not hits: - sys.stderr.write( - f" ! '{title}' has no generated class; " - "constraint not enforced\n" - ) - return patched, 1 - return patched, 0 - - -def _patch_max_properties(): - """Inject maxProperties validators; return (patched_count, exit_code).""" - constraints = find_root_max_properties(SCHEMA_DIR) - if not constraints: - sys.stdout.write( - "postprocess: no root-level maxProperties constraints found\n" - ) - return 0, 0 - patched = 0 - for title, maximum in sorted(constraints.items()): - hits = [] - for path in sorted(OUTPUT_DIR.rglob("*.py")): - source = path.read_text(encoding="utf-8") - if not re.search(rf"^class {re.escape(title)}\(", source, re.M): - continue - updated = inject_max_properties(source, title, maximum) - if updated != source: - path.write_text(updated, encoding="utf-8") - patched += 1 - hits.append(path) - label = ", ".join(str(h) for h in hits) or "NO GENERATED CLASS FOUND" - sys.stdout.write(f" maxProperties={maximum} on '{title}' -> {label}\n") - if not hits: - sys.stderr.write( - f" ! '{title}' has no generated class; " - "constraint not enforced\n" - ) - return patched, 1 - return patched, 0 - - -def _array_contains_targets(): - """Resolve ``title -> groups`` for every model needing a contains bound. - - The authoritative (complete) groups come from the pristine schemas. The - preprocessed tree is consulted only to enumerate which models actually - exist — the base plus its generated request variants — so each variant - inherits its base schema's full set of containment rules. Variants are - linked to their base by file stem (``totals_create_request`` -> ``totals``). - """ - raw = find_array_contains_constraints(RAW_SCHEMA_DIR) - if not raw: - # Fallback keeps a standalone run working if the snapshot is absent, - # though the pipeline always provides it (see module docstring). - raw = find_array_contains_constraints(SCHEMA_DIR) - if raw: - sys.stderr.write( - f" ! {RAW_SCHEMA_DIR} missing; falling back to preprocessed " - "schemas (multi-branch contains may be incomplete)\n" - ) - if not raw: - return {} - raw_stems = sorted(raw, key=len, reverse=True) - targets = {} - # Enumerate base + variants from the preprocessed tree; attach raw groups. - for stem, info in find_array_contains_constraints(SCHEMA_DIR).items(): - origin = next( - (s for s in raw_stems if stem == s or stem.startswith(s + "_")), - None, - ) - if origin is not None: - targets[info["title"]] = { - "groups": raw[origin]["groups"], - "item_condition": raw[origin]["item_condition"], - } - # Defensive: cover each raw base title even if the preprocessed base lost - # its contains entirely. - for info in raw.values(): - targets.setdefault( - info["title"], - { - "groups": info["groups"], - "item_condition": info["item_condition"], - }, - ) - return targets - - -def _patch_property_names(): - """Inject propertyNames validators; return (patched_count, exit_code).""" - patterns = find_property_names_patterns(SCHEMA_DIR) - if not patterns: - sys.stdout.write( - "postprocess: no propertyNames constraints on extra-allow " - "models found\n" - ) - return 0, 0 - patched = 0 - for class_name, pattern in sorted(patterns.items()): - hits = [] - for path in sorted(OUTPUT_DIR.rglob("*.py")): - source = path.read_text(encoding="utf-8") - if not re.search( - rf"^class {re.escape(class_name)}\(", source, re.M - ): - continue - updated = inject_property_names(source, class_name, pattern) - if updated != source: - path.write_text(updated, encoding="utf-8") - patched += 1 - hits.append(path) - label = ", ".join(str(h) for h in hits) or "NO GENERATED CLASS FOUND" - sys.stdout.write( - f" propertyNames {pattern!r} on '{class_name}' -> {label}\n" - ) - if not hits: - sys.stderr.write( - f" ! '{class_name}' has no generated class; " - "constraint not enforced\n" - ) - return patched, 1 - return patched, 0 - - -def _patch_array_contains(): - """Inject array-contains validators; return (patched_count, exit_code).""" - targets = _array_contains_targets() - if not targets: - sys.stdout.write("postprocess: no array contains constraints found\n") - return 0, 0 - patched = 0 - for title, constraint in sorted(targets.items()): - groups = constraint["groups"] - alias = _alias_name(title) - hits = [] - for path in sorted(OUTPUT_DIR.rglob("*.py")): - source = path.read_text(encoding="utf-8") - if not re.search( - rf"^{re.escape(alias)} = TypeAliasType\(", source, re.M - ): - continue - updated = inject_array_contains( - source, alias, groups, constraint["item_condition"] - ) - if updated != source: - path.write_text(updated, encoding="utf-8") - patched += 1 - hits.append(path) - preds = "; ".join( - " & ".join(f"{f}=={c!r}" for f, c in g["pairs"]) for g in groups - ) - label = ", ".join(str(h) for h in hits) or "NO GENERATED ALIAS FOUND" - sys.stdout.write(f" contains [{preds}] on '{title}' -> {label}\n") - if not hits: - sys.stderr.write( - f" ! '{title}' has no generated alias; " - "constraint not enforced\n" - ) - return patched, 1 - return patched, 0 - - -def _patch_conditional_required(): - """Inject conditional-required validators; return counts and status.""" - rules_by_class = find_conditional_required(SCHEMA_DIR) - if not rules_by_class: - sys.stdout.write( - "postprocess: no simple conditional required rules found\n" - ) - return 0, 0 - patched = 0 - for class_name, rules in sorted(rules_by_class.items()): - hits = [] - for path in sorted(OUTPUT_DIR.rglob("*.py")): - source = path.read_text(encoding="utf-8") - if not re.search( - rf"^class {re.escape(class_name)}\(", source, re.M - ): - continue - updated = inject_conditional_required(source, class_name, rules) - if updated != source: - path.write_text(updated, encoding="utf-8") - patched += 1 - hits.append(path) - label = ( - ", ".join(str(path) for path in hits) or "NO GENERATED CLASS FOUND" - ) - sys.stdout.write( - f" conditional required on '{class_name}' -> {label}\n" - ) - if not hits: - return patched, 1 - return patched, 0 - - -def _patch_conditional_bounds(): - """Inject conditional numeric-bound validators; return counts and status.""" - rules_by_class = find_conditional_bounds(SCHEMA_DIR) - if not rules_by_class: - sys.stdout.write("postprocess: no conditional bounds rules found\n") - return 0, 0 - patched = 0 - for class_name, rules in sorted(rules_by_class.items()): - hits = [] - for path in sorted(OUTPUT_DIR.rglob("*.py")): - source = path.read_text(encoding="utf-8") - if not re.search( - rf"^class {re.escape(class_name)}\(", source, re.M - ): - continue - updated = inject_conditional_bounds(source, class_name, rules) - if updated != source: - path.write_text(updated, encoding="utf-8") - patched += 1 - hits.append(path) - label = ( - ", ".join(str(path) for path in hits) or "NO GENERATED CLASS FOUND" - ) - sys.stdout.write(f" conditional bounds on '{class_name}' -> {label}\n") - if not hits: - return patched, 1 - return patched, 0 - - -def _patch_conditional_array_retyping(): - """Inject array-item retyping checks; return counts and status.""" - rules_by_class = find_conditional_array_retyping(SCHEMA_DIR) - if not rules_by_class: - sys.stdout.write( - "postprocess: no conditional array-item retyping rules found\n" - ) - return 0, 0 - patched = 0 - for class_name, rules in sorted(rules_by_class.items()): - hits = [] - for path in sorted(OUTPUT_DIR.rglob("*.py")): - source = path.read_text(encoding="utf-8") - if not re.search( - rf"^class {re.escape(class_name)}\(", source, re.M - ): - continue - updated = inject_conditional_array_retyping( - source, class_name, rules - ) - if updated != source: - path.write_text(updated, encoding="utf-8") - patched += 1 - hits.append(path) - label = ( - ", ".join(str(path) for path in hits) or "NO GENERATED CLASS FOUND" - ) - sys.stdout.write( - f" conditional array-item retyping on '{class_name}' -> {label}\n" - ) - if not hits: - return patched, 1 - return patched, 0 - - -def _patch_dependent_required(): - """Inject dependentRequired validators; return counts and status.""" - rules_by_class = find_root_dependent_required(SCHEMA_DIR) - if not rules_by_class: - sys.stdout.write( - "postprocess: no dependentRequired constraints found\n" - ) - return 0, 0 - patched = 0 - for class_name, rules in sorted(rules_by_class.items()): - hits = [] - for path in sorted(OUTPUT_DIR.rglob("*.py")): - source = path.read_text(encoding="utf-8") - match = re.search( - rf"^class {re.escape(class_name)}\(", source, re.M - ) - if match is None: - continue - tail = re.compile(r"^\S", re.M) - end_match = tail.search(source, match.end()) - end = end_match.start() if end_match else len(source) - if ( - f"def {_DEPENDENT_REQUIRED_MARKER}(" - in source[match.start() : end] - ): - hits.append(path) - continue - updated = inject_dependent_required(source, class_name, rules) - if updated == source: - continue - path.write_text(updated, encoding="utf-8") - patched += 1 - hits.append(path) - label = ( - ", ".join(str(path) for path in hits) or "NO APPLICABLE CLASS FOUND" - ) - sys.stdout.write(f" dependentRequired on '{class_name}' -> {label}\n") - if not hits: - return patched, 1 - return patched, 0 - - -def _patch_unique_items(): - """Inject uniqueItems validators; return (patched_count, exit_code).""" - unique_fields_by_class = find_unique_items_fields(SCHEMA_DIR) - if not unique_fields_by_class: - sys.stdout.write("postprocess: no uniqueItems constraints found\n") - return 0, 0 - unique_patched = 0 - touched = [] - for path in sorted(OUTPUT_DIR.rglob("*.py")): - source = path.read_text(encoding="utf-8") - updated = inject_unique_items(source, unique_fields_by_class) - if updated != source: - path.write_text(updated, encoding="utf-8") - unique_patched += 1 - touched.append(path) - labels = sorted( - f"{class_name}.{field}" - for class_name, fields in unique_fields_by_class.items() - for field in fields - ) - sys.stdout.write( - f" uniqueItems fields {labels} -> " - f"{unique_patched} module(s) patched" - f" ({', '.join(str(t) for t in touched) or 'none'})\n" - ) - return unique_patched, 0 - - -def find_extra_forbid_class_names(schema_dir): - """Map generated class names for objects that forbid unknown keys. - - The gap this targets: an object schema that declares - ``additionalProperties: false`` AND carries named ``properties`` is still - emitted by the generator as ``BaseModel(extra="allow")`` (generation runs - with ``--extra-fields=allow``), so unknown keys are silently retained in - ``model_extra`` instead of being rejected. The rule is mechanical: an - object node with ``additionalProperties is False`` and non-empty named - ``properties`` maps to its generated class name via its ``title`` (root - objects) or its property path (untitled nested objects, e.g. - ``allows_multi_destination`` -> ``AllowsMultiDestination``). - """ - found = set() - - def visit(node, class_name): - if not isinstance(node, dict): - if isinstance(node, list): - for item in node: - visit(item, class_name) - return - effective = ( - _alias_name(node["title"]) if node.get("title") else class_name - ) - if ( - node.get("additionalProperties") is False - and isinstance(node.get("properties"), dict) - and node["properties"] - ): - found.add(effective) - for name, child in (node.get("properties") or {}).items(): - visit(child, _to_camel_case(name)) - - for path in sorted(Path(schema_dir).rglob("*.json")): - try: - schema = json.loads(path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): - continue - if not isinstance(schema, dict): - continue - root_name = ( - _alias_name(schema["title"]) - if schema.get("title") - else _to_camel_case(path.stem) - ) - visit(schema, root_name) - return found - - -def inject_extra_forbid(source, class_name): - """Flip the target class's ``extra="allow"`` config to ``extra="forbid"``. - - Only the named class's own ``model_config`` is changed (its body, from the - ``class`` statement to the next top-level ``class``/``def``), so sibling - classes in the same module keep ``extra="allow"``. The source is returned - unchanged when the class is absent or already ``extra="forbid"``. - """ - head = re.search( - rf"^class {re.escape(class_name)}\(BaseModel\):", source, re.M - ) - if not head: - return source - rest = source[head.end() :] - next_top = re.search(r"^(?=class |def )", rest, re.M) - body_end = len(rest) if next_top is None else next_top.start() - body = rest[:body_end] - if 'extra="allow"' not in body: - return source - new_body = body.replace('extra="allow"', 'extra="forbid"', 1) - return source[: head.end()] + new_body + rest[body_end:] - - -def _patch_extra_forbid(): - """Inject extra="forbid" on models whose schema forbids unknown keys.""" - class_names = find_extra_forbid_class_names(SCHEMA_DIR) - if not class_names: - sys.stdout.write( - "postprocess: no additionalProperties:false models found\n" - ) - return 0, 0 - patched = 0 - for class_name in sorted(class_names): - hits = [] - for path in sorted(OUTPUT_DIR.rglob("*.py")): - source = path.read_text(encoding="utf-8") - if not re.search( - rf"^class {re.escape(class_name)}\(", source, re.M - ): - continue - updated = inject_extra_forbid(source, class_name) - if updated != source: - path.write_text(updated, encoding="utf-8") - patched += 1 - hits.append(path) - label = ", ".join(str(h) for h in hits) or "NO GENERATED CLASS FOUND" - sys.stdout.write(f" extra=forbid on '{class_name}' -> {label}\n") - if not hits: - sys.stderr.write( - f" ! '{class_name}' has no generated class; " - "constraint not enforced\n" - ) - return patched, 1 - return patched, 0 - - -def main(): - """Main entry point to scan schemas and patch generated models.""" - patched_mp, rc_mp = _patch_min_properties() - patched_xp, rc_xp = _patch_max_properties() - patched_pn, rc_pn = _patch_property_names() - patched_ac, rc_ac = _patch_array_contains() - patched_cr, rc_cr = _patch_conditional_required() - patched_cb, rc_cb = _patch_conditional_bounds() - patched_rt, rc_rt = _patch_conditional_array_retyping() - patched_dr, rc_dr = _patch_dependent_required() - patched_ui, rc_ui = _patch_unique_items() - patched_ef, rc_ef = _patch_extra_forbid() - total = ( - patched_mp - + patched_xp - + patched_pn - + patched_ac - + patched_cr - + patched_cb - + patched_rt - + patched_dr - + patched_ui - + patched_ef - ) - sys.stdout.write(f"postprocess: {total} module(s) patched\n") - return ( - rc_mp - or rc_xp - or rc_pn - or rc_ac - or rc_cr - or rc_cb - or rc_rt - or rc_dr - or rc_ui - or rc_ef - ) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/src/ucp_sdk/models/schemas/capability.py b/src/ucp_sdk/models/schemas/capability.py index 3538563..e49155c 100644 --- a/src/ucp_sdk/models/schemas/capability.py +++ b/src/ucp_sdk/models/schemas/capability.py @@ -1,176 +1,19 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Capability models.""" from __future__ import annotations -from typing import Annotated, Any - -from pydantic import AnyUrl, BaseModel, ConfigDict, Field -from typing_extensions import TypeAliasType - -from .common.types import reverse_domain_name - -UcpCapability = TypeAliasType( - "UcpCapability", Annotated[Any, Field(..., title="UCP Capability")] +from .models import ( + CapabilityBase, + CapabilityBusinessSchema, + CapabilityPlatformSchema, + CapabilityResponseSchema, ) -""" -Schema for UCP capabilities and extensions. Extensions are capabilities with an 'extends' field. Uses reverse-domain naming for governance. -""" - - -Extends = TypeAliasType( - "Extends", - Annotated[ - list[reverse_domain_name.ReverseDomainName], Field(..., min_length=1) - ], -) -""" -Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. -""" - - -class Base(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - version: str = Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$") - """ - Entity version in YYYY-MM-DD format. - """ - spec: AnyUrl | None = None - """ - URL to human-readable specification document. - """ - schema_: AnyUrl | None = Field(None, alias="schema") - """ - URL to JSON Schema defining this entity's structure and payloads. - """ - id: str | None = None - """ - Unique identifier for this entity instance. Used to disambiguate when multiple instances exist. - """ - config: dict[str, Any] | None = None - """ - Entity-specific configuration. Structure defined by each entity's schema. - """ - extends: reverse_domain_name.ReverseDomainName | Extends | None = None - """ - Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. - """ - - -class PlatformSchema(BaseModel): - """ - Full capability declaration for platform-level discovery. Includes spec/schema URLs for agent fetching. - """ - - model_config = ConfigDict( - extra="allow", - ) - version: str = Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$") - """ - Entity version in YYYY-MM-DD format. - """ - spec: AnyUrl - """ - URL to human-readable specification document. - """ - schema_: AnyUrl = Field(..., alias="schema") - """ - URL to JSON Schema defining this entity's structure and payloads. - """ - id: str | None = None - """ - Unique identifier for this entity instance. Used to disambiguate when multiple instances exist. - """ - config: dict[str, Any] | None = None - """ - Entity-specific configuration. Structure defined by each entity's schema. - """ - extends: reverse_domain_name.ReverseDomainName | Extends | None = None - """ - Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. - """ - - -class BusinessSchema(BaseModel): - """ - Capability declaration for business/merchant discovery. Requires the `schema` URL so platforms can fetch and compose it during negotiation; may also include business-specific config overrides. - """ - - model_config = ConfigDict( - extra="allow", - ) - version: str = Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$") - """ - Entity version in YYYY-MM-DD format. - """ - spec: AnyUrl | None = None - """ - URL to human-readable specification document. - """ - schema_: AnyUrl = Field(..., alias="schema") - """ - URL to JSON Schema defining this entity's structure and payloads. - """ - id: str | None = None - """ - Unique identifier for this entity instance. Used to disambiguate when multiple instances exist. - """ - config: dict[str, Any] | None = None - """ - Entity-specific configuration. Structure defined by each entity's schema. - """ - extends: reverse_domain_name.ReverseDomainName | Extends | None = None - """ - Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. - """ - - -class ResponseSchema(BaseModel): - """ - Capability reference in responses. Only name/version required to confirm active capabilities. - """ - model_config = ConfigDict( - extra="allow", - ) - version: str = Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$") - """ - Entity version in YYYY-MM-DD format. - """ - spec: AnyUrl | None = None - """ - URL to human-readable specification document. - """ - schema_: AnyUrl | None = Field(None, alias="schema") - """ - URL to JSON Schema defining this entity's structure and payloads. - """ - id: str | None = None - """ - Unique identifier for this entity instance. Used to disambiguate when multiple instances exist. - """ - config: dict[str, Any] | None = None - """ - Entity-specific configuration. Structure defined by each entity's schema. - """ - extends: reverse_domain_name.ReverseDomainName | Extends | None = None - """ - Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. - """ +Base = CapabilityBase +__all__ = [ + "CapabilityBase", + "CapabilityBusinessSchema", + "CapabilityPlatformSchema", + "CapabilityResponseSchema", + "Base", +] diff --git a/src/ucp_sdk/models/schemas/common/__init__.py b/src/ucp_sdk/models/schemas/common/__init__.py index 1252d6b..d2182a2 100644 --- a/src/ucp_sdk/models/schemas/common/__init__.py +++ b/src/ucp_sdk/models/schemas/common/__init__.py @@ -1,17 +1,3 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +"""Common models.""" -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +from .types import * # noqa: F403 diff --git a/src/ucp_sdk/models/schemas/common/identity_linking.py b/src/ucp_sdk/models/schemas/common/identity_linking.py deleted file mode 100644 index a298779..0000000 --- a/src/ucp_sdk/models/schemas/common/identity_linking.py +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated, Any - -from pydantic import BaseModel, ConfigDict, Field -from typing_extensions import TypeAliasType - -from ..capability import BusinessSchema, PlatformSchema -from .types import description as description_1 -from .types import reverse_domain_name - -IdentityLinking = TypeAliasType( - "IdentityLinking", Annotated[Any, Field(..., title="Identity Linking")] -) -""" -Capability schema for identity linking. Businesses declare the user-authenticated scopes they offer in a flat 'scopes' map. Each key is the OAuth scope string as it appears on the wire ('{capability}:{scope}', e.g. 'dev.ucp.shopping.order:read'). Scope presence implies that the corresponding operations require user authentication. Operations not gated by any listed scope operate at whatever access level the business permits; UCP does not prescribe a default. -""" - - -class ScopePolicy(BaseModel): - """ - Per-scope policy and metadata — auth constraints (e.g. min_acr, max_token_age), declarative metadata (e.g. claims produced, consent descriptions), or any other scope-specific configuration. An empty object means user authentication is required with no additional policy. Open for non-breaking extension. - """ - - model_config = ConfigDict( - extra="allow", - ) - description: description_1.Description | None = None - """ - Optional human-readable description of the scope that platforms can use to present and explain context (requirement and value) to the user. - """ - - -ScopeToken = TypeAliasType( - "ScopeToken", - Annotated[ - str, - Field( - ..., - pattern="^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?)+:[a-z][a-z0-9_]*$", - ), - ], -) -""" -OAuth scope string formed by joining a capability name and a scope name with a colon: '{capability}:{scope}', e.g. 'dev.ucp.shopping.order:read'. Capability names use reverse-DNS naming; scope names denote the permission granted, defined by each capability's spec (e.g. 'read', 'manage', 'create'). Platforms request these strings verbatim in OAuth 'scope' parameters; issued tokens carry them in the 'scope' claim. -""" - - -class Provider(BaseModel): - """ - A trusted identity provider for delegated authentication, keyed by the 'type' discriminator. 'oauth2' denotes an OAuth 2.0 / OIDC authorization server. Future versions MAY define additional types (e.g. wallet attestation) as non-breaking extensions; platforms MUST treat entries whose 'type' they do not support as filtered out (see Provider Selection). - """ - - model_config = ConfigDict( - extra="allow", - ) - type: str - """ - Provider mechanism discriminator. 'oauth2' for OAuth 2.0 / OIDC authorization servers. Additional values MAY be defined by future versions; the value is an open string, not a closed enum, so unrecognized types remain valid and are filtered at runtime. - """ - - -class Config(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - providers: ( - dict[reverse_domain_name.ReverseDomainName, list[Provider]] | None - ) = None - """ - Map of trusted external identity providers keyed by reverse-domain identifier. Each key maps to an array of mechanism entries — an IdP namespace MAY offer multiple token acquisition mechanisms. Declares which upstream IdPs the business will accept JWT bearer assertions from for the Accelerated IdP Flow (chaining via RFC 8693 + RFC 7523). This field is additive: direct OAuth against the business domain via RFC 8414 discovery is always available regardless of 'providers' content. Businesses MUST NOT list their own authorization server here — chaining-to-self is degenerate, and direct OAuth covers that path. When absent, empty, or when no listed mechanism is supported by the platform, platforms run direct OAuth on the business domain. - """ - scopes: dict[ScopeToken, ScopePolicy] - """ - Map of user-authenticated scopes offered by this business. Each key is an OAuth scope string formed as '{capability}:{scope}' (e.g. 'dev.ucp.shopping.order:read'). Scope presence in this map declares that the corresponding operations require a user identity token. Operations not gated by any listed scope operate at whatever access level the business permits; UCP does not prescribe a default. Each value is a per-scope policy object (empty object means user auth required with no additional policy). - """ - - -class IdentityLinkingPlatformSchema(PlatformSchema): - """ - Platform-level identity linking capability declaration. Platforms advertise support for identity linking; no auth-specific config is required. - """ - - model_config = ConfigDict( - extra="allow", - ) - - -class IdentityLinkingBusinessSchema(BusinessSchema): - """ - Business-level identity linking configuration. Businesses declare the user-authenticated scopes they offer in 'config.scopes'. - """ - - model_config = ConfigDict( - extra="allow", - ) - config: Config diff --git a/src/ucp_sdk/models/schemas/common/location_lookup.py b/src/ucp_sdk/models/schemas/common/location_lookup.py deleted file mode 100644 index 39c2714..0000000 --- a/src/ucp_sdk/models/schemas/common/location_lookup.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field - -from .. import ucp as ucp_1 -from .types import context as context_1 -from .types import location_distance, location_filter, location_serves, message -from .types import signals as signals_1 -from .types.location import Location - - -class LocationLookup(BaseModel): - """ - Location lookup by identifiers. Supports batch retrieval and single-location detail. - """ - - model_config = ConfigDict( - extra="allow", - ) - - -class Input(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - The identifier exactly as supplied in the lookup request. - """ - - -class LookupRequest(BaseModel): - """ - Request body for batch location lookup. The Business resolves and deduplicates `ids` before applying `distance`, `serves`, and every supplied `filters` predicate; all structured predicates combine with AND. - """ - - model_config = ConfigDict( - extra="allow", - ) - ids: list[str] = Field(..., min_length=1) - """ - Identifiers of the Locations to look up. The Business MUST support canonical `Location.id` values and MAY support secondary or alias identifiers. - """ - distance: location_distance.LocationDistance | None = None - """ - Optional explicit-center radius predicate applied after ID resolution. It combines with `serves` and every supplied `filters` predicate using AND. - """ - serves: location_serves.LocationServes | None = None - """ - Optional authoritative service-target predicate applied after ID resolution. It combines with `distance` and every supplied `filters` predicate using AND. - """ - filters: location_filter.LocationFilter | None = None - context: context_1.Context | None = None - signals: signals_1.Signals | None = None - - -class LookupLocation(Location): - """ - Location with required correlation metadata for lookup responses. - """ - - model_config = ConfigDict( - extra="allow", - ) - inputs: list[Input] = Field(..., min_length=1) - """ - Which request identifiers resolved to this Location. Each entry preserves one identifier exactly as supplied in the request. - """ - - -class LookupResponse(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - ucp: ucp_1.ResponseLocationSchema - locations: list[LookupLocation] - """ - Locations matching the requested identifiers and refinements. May contain fewer Locations if some identifiers do not resolve or their resolved Locations are filtered out, or more if one identifier resolves to multiple Locations. When multiple identifiers resolve to the same Location, one returned Location carries all corresponding `inputs` entries. - """ - messages: list[message.Message] | None = None - """ - Errors, warnings, or informational messages about the requested Locations, including `batch_limit_applied` when the Business processes only its configured maximum number of identifiers. - """ diff --git a/src/ucp_sdk/models/schemas/common/location_search.py b/src/ucp_sdk/models/schemas/common/location_search.py deleted file mode 100644 index 35e0983..0000000 --- a/src/ucp_sdk/models/schemas/common/location_search.py +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - -from .. import ucp as ucp_1 -from .types import context as context_1 -from .types import ( - location, - location_distance, - location_filter, - location_serves, - message, -) -from .types import pagination as pagination_1 -from .types import signals as signals_1 - - -class LocationSearch(BaseModel): - """ - Location search capability. Supports natural language queries, distance and serviceability relations, structured filtering including current item availability, and pagination. - """ - - model_config = ConfigDict( - extra="allow", - ) - - -class SearchRequest(BaseModel): - """ - Request body for location search. The `distance` and `serves` relations and every supplied `filters` predicate combine with AND; `query` does not relax them. - """ - - model_config = ConfigDict( - extra="allow", - ) - query: str | None = None - """ - Free-text search query for natural language location search (e.g., 'restaurants near me that deliver', 'hotels with pool'). - """ - context: context_1.Context | None = None - signals: signals_1.Signals | None = None - distance: location_distance.LocationDistance | None = None - """ - Optional explicit-center radius predicate. When present, it combines with `serves` and every supplied `filters` predicate using AND. - """ - serves: location_serves.LocationServes | None = None - """ - Optional authoritative service-target predicate. When present, it combines with `distance` and every supplied `filters` predicate using AND. - """ - filters: location_filter.LocationFilter | None = None - pagination: pagination_1.Request | None = None - - -class SearchResponse(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - ucp: ucp_1.ResponseLocationSchema - locations: list[location.Location] - """ - Locations matching the search criteria. - """ - pagination: pagination_1.Response | None = None - messages: list[message.Message] | None = None - """ - Errors, warnings, or informational messages about the search results. - """ diff --git a/src/ucp_sdk/models/schemas/common/loyalty.py b/src/ucp_sdk/models/schemas/common/loyalty.py deleted file mode 100644 index 009222d..0000000 --- a/src/ucp_sdk/models/schemas/common/loyalty.py +++ /dev/null @@ -1,297 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated, Any - -from pydantic import BaseModel, ConfigDict, Field -from typing_extensions import TypeAliasType - -from ..shopping.cart import Cart as Cart_1 -from ..shopping.catalog_lookup import GetProductResponse, LookupResponse -from ..shopping.catalog_search import SearchResponse -from ..shopping.checkout import Checkout as Checkout_1 -from .types import reverse_domain_name - -LoyaltyExtension = TypeAliasType( - "LoyaltyExtension", Annotated[Any, Field(..., title="Loyalty Extension")] -) -""" -Extends various Capabilities with loyalty support using memberships info. -""" - - -RewardAmount = TypeAliasType("RewardAmount", Annotated[int, Field(..., ge=0)]) -""" -Non-negative integer amount denominated in the minor unit of the associated reward currency. The associated reward currency's `decimal_places` defines the minor-to-major ratio and defaults to 0 when omitted. -""" - - -class EarningBreakdown(BaseModel): - """ - Breakdown rule of the reward earnings - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Unique rewards breakdown rule identifier. - """ - amount: RewardAmount - """ - Rewards earned from this rule. - """ - description: str - """ - A display-ready, human-readable rationale for the specific rewards (e.g. 2x on footwear). - """ - benefit_id: str | None = None - """ - Optional `id` of the membership_tier_benefit that produced this rewards rule. Resolves against `membership_tier_benefit.id` within the same parent loyalty membership. - """ - - -class EarningForecast(BaseModel): - """ - Preview of rewards to be earned from the current transaction. - """ - - model_config = ConfigDict( - extra="allow", - ) - amount: RewardAmount - """ - Total rewards to be earned if the transaction completes. - """ - breakdown: list[EarningBreakdown] | None = None - """ - List of breakdown of earning contributing to the total. - """ - - -class RewardCurrency(BaseModel): - """ - The currency of the loyalty reward. - """ - - model_config = ConfigDict( - extra="allow", - ) - name: str - """ - Human-readable name of the currency (e.g. 'LoyaltyStars'). - """ - code: str - """ - Business-specific representation of the currency (e.g. 'LST'). - """ - decimal_places: int | None = Field(0, ge=0) - """ - The position of a digit to the right of a decimal point. Applies to all amount related fields for rewards. - """ - - -class Currency(BaseModel): - """ - A unit of value that customers can accumulate through various commercial activities. - """ - - model_config = ConfigDict( - extra="allow", - ) - name: str - """ - Human-readable name of the currency (e.g. 'LoyaltyStars'). - """ - code: str - """ - Business-specific representation of the currency (e.g. 'LST'). - """ - decimal_places: int | None = Field(0, ge=0) - """ - The position of a digit to the right of a decimal point. Applies to all amount related fields for rewards. - """ - - -class MembershipReward(BaseModel): - """ - Quantifiable reward type and optional earning forecast for the current transaction. - """ - - model_config = ConfigDict( - extra="allow", - ) - currency: Currency - """ - A unit of value that customers can accumulate through various commercial activities. - """ - earning_forecast: EarningForecast | None = None - """ - Preview of rewards to be earned from the current transaction. - """ - - -class MembershipTierBenefit(BaseModel): - """ - Benefits associated with a membership tier. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Unique identifier for the tier benefit. - """ - description: str - """ - A display-ready, human-readable explanation of this benefit (e.g. 'Early access to sales'). - """ - - -class MembershipTier(BaseModel): - """ - Specific achievement rank or status milestone that unlocks escalating value as a member progresses through activity or spend. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Unique identifier for the membership tier. - """ - name: str - """ - The human-readable name of the tier (e.g., 'Platinum'). - """ - benefits: list[MembershipTierBenefit] | None = None - """ - List of benefits associated with this tier. - """ - - -class LoyaltyMembership(BaseModel): - """ - Loyalty membership the business has accepted for the eligibility claim represented by the parent map key. Programs that can be joined independently MUST be modeled as separate sibling entries under the loyalty map, distinguished by reverse-domain naming (e.g., 'com.example.rewards' and 'com.example.rewards.card'). - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Unique loyalty membership identifier. - """ - name: str - """ - Business specific name of the loyalty membership/program. - """ - display_id: str | None = None - """ - A masked or partial version of the membership id for user recognition (e.g., '****5678'). MUST NOT be set if the membership has not been verified. - """ - tiers: list[MembershipTier] | None = None - """ - Active or display-safe tier context for this membership. Most programs are single-status (one entry); programs with parallel status dimensions (e.g., current and lifetime) populate one entry per active tier. Omitted when no tier context has been resolved. - """ - rewards: list[MembershipReward] | None = None - """ - Reward types and earning forecasts associated with this membership. Each object encapsulates one type of reward. - """ - provisional: bool - """ - True if this membership requires additional verification. - """ - - -Loyalty = TypeAliasType( - "Loyalty", dict[reverse_domain_name.ReverseDomainName, LoyaltyMembership] -) -""" -Key-value map whose keys represent buyer/platform asserted eligibility claims and whose values represent associated membership information. All loyalty keys MUST use reverse-domain naming to ensure provenance and prevent collisions when multiple extensions contribute to the shared namespace. -""" - - -class Search(SearchResponse): - """ - Catalog Search response extended with Loyalty capability. - """ - - model_config = ConfigDict( - extra="allow", - ) - loyalty: Loyalty | None = None - - -class Lookup1(LookupResponse): - """ - Catalog Lookup response extended with Loyalty capability. - """ - - model_config = ConfigDict( - extra="allow", - ) - loyalty: Loyalty | None = None - - -class Lookup2(GetProductResponse): - """ - Catalog Lookup response extended with Loyalty capability. - """ - - model_config = ConfigDict( - extra="allow", - ) - loyalty: Loyalty | None = None - - -Lookup = TypeAliasType( - "Lookup", - Annotated[ - Lookup1 | Lookup2, Field(..., title="Catalog Lookup with Loyalty") - ], -) -""" -Catalog Lookup response extended with Loyalty capability. -""" - - -class Cart(Cart_1): - """ - Cart extended with Loyalty capability. - """ - - model_config = ConfigDict( - extra="allow", - ) - loyalty: Loyalty | None = None - - -class Checkout(Checkout_1): - """ - Checkout extended with Loyalty capability. - """ - - model_config = ConfigDict( - extra="allow", - ) - loyalty: Loyalty | None = None diff --git a/src/ucp_sdk/models/schemas/common/payment_ap2_mandate.py b/src/ucp_sdk/models/schemas/common/payment_ap2_mandate.py deleted file mode 100644 index d8406b4..0000000 --- a/src/ucp_sdk/models/schemas/common/payment_ap2_mandate.py +++ /dev/null @@ -1,146 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated, Any, Literal - -from pydantic import BaseModel, ConfigDict, Field -from typing_extensions import TypeAliasType - -from ..shopping.checkout import Checkout as Checkout_1 - -PaymentAp2MandateExtension = TypeAliasType( - "PaymentAp2MandateExtension", - Annotated[Any, Field(..., title="Payment AP2 Mandate Extension")], -) -""" -Extends Checkout with cryptographic mandate support for non-repudiable authorization per the AP2 protocol. Uses embedded signature model with ap2 namespace. -""" - - -MerchantAuthorization = TypeAliasType( - "MerchantAuthorization", - Annotated[ - str, - Field( - ..., - pattern="^[A-Za-z0-9_-]+\\.\\.[A-Za-z0-9_-]+$", - title="Merchant Authorization", - ), - ], -) -""" -JWS Detached Content signature (RFC 7515 Appendix F) over the checkout response body (excluding ap2 field). Format: `..`. The header MUST contain 'alg' (ES256/ES384/ES512) and 'kid' claims. The signature covers both the header and JCS-canonicalized checkout payload. -""" - - -CheckoutMandate = TypeAliasType( - "CheckoutMandate", - Annotated[ - str, - Field( - ..., - pattern="^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]*\\.[A-Za-z0-9_-]+(~[A-Za-z0-9_-]+)*$", - title="Checkout Mandate", - ), - ], -) -""" -SD-JWT+kb credential in `ap2.checkout_mandate`. Proving user authorization for the checkout. Contains the full checkout including `ap2.merchant_authorization`. -""" - - -class Ap2WithMerchantAuthorization(BaseModel): - """ - AP2 extension data including merchant authorization. - """ - - model_config = ConfigDict( - extra="allow", - ) - merchant_authorization: MerchantAuthorization | None = None - """ - Merchant's signature proving checkout terms are authentic. - """ - - -class Ap2WithCheckoutMandate(BaseModel): - """ - AP2 extension data including checkout mandate. - """ - - model_config = ConfigDict( - extra="allow", - ) - checkout_mandate: CheckoutMandate | None = None - """ - SD-JWT+kb proving user authorized this checkout. - """ - - -ErrorCode = TypeAliasType( - "ErrorCode", - Annotated[ - Literal[ - "mandate_required", - "agent_missing_key", - "mandate_invalid_signature", - "mandate_expired", - "mandate_scope_mismatch", - "merchant_authorization_invalid", - "merchant_authorization_missing", - ], - Field(..., title="AP2 Error Code"), - ], -) -""" -Error codes specific to AP2 mandate verification. -""" - - -class Ap2(BaseModel): - """ - AP2 extension data including merchant authorization. - """ - - model_config = ConfigDict( - extra="allow", - ) - merchant_authorization: MerchantAuthorization | None = None - """ - Merchant's signature proving checkout terms are authentic. - """ - checkout_mandate: CheckoutMandate | None = None - """ - SD-JWT+kb proving user authorized this checkout. - """ - - -class Checkout(Checkout_1): - """ - Checkout extended with AP2 mandate support. - """ - - model_config = ConfigDict( - extra="allow", - ) - ap2: Ap2 | None = None - """ - AP2 extension data including merchant authorization. - """ diff --git a/src/ucp_sdk/models/schemas/common/payment_authentication.py b/src/ucp_sdk/models/schemas/common/payment_authentication.py deleted file mode 100644 index 927a069..0000000 --- a/src/ucp_sdk/models/schemas/common/payment_authentication.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated, Any - -from pydantic import AnyUrl, BaseModel, ConfigDict, Field -from typing_extensions import TypeAliasType - -from ..shopping.checkout import Checkout as Checkout_1 - -PaymentAuthenticationExtension = TypeAliasType( - "PaymentAuthenticationExtension", - Annotated[Any, Field(..., title="Payment Authentication Extension")], -) -""" -Extends capabilities (e.g., checkout in retail shopping) with standard device data collection and 3DS challenge Action types used during payment authentication. -""" - - -class Config(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - payment_instrument_id: str = Field(..., min_length=1) - """ - ID of the payment instrument in the containing Checkout associated with this device data collection Action. - """ - url: AnyUrl - """ - URL for the invisible device data collection surface. - """ - - -class DevUcpCommonPaymentDeviceDataCollectionItem(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - config: Config - - -class Config2(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - payment_instrument_id: str = Field(..., min_length=1) - """ - ID of the payment instrument in the containing Checkout associated with this 3DS challenge Action. - """ - url: AnyUrl - """ - URL for the buyer-facing 3DS challenge surface. - """ - - -class DevUcpCommonPaymentThreeDsChallengeItem(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - config: Config2 - - -class Actions(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - dev_ucp_common_payment_device_data_collection: ( - list[DevUcpCommonPaymentDeviceDataCollectionItem] | None - ) = Field(None, alias="dev.ucp.common.payment.device_data_collection") - """ - A 3DS device data collection Action. - """ - dev_ucp_common_payment_three_ds_challenge: ( - list[DevUcpCommonPaymentThreeDsChallengeItem] | None - ) = Field(None, alias="dev.ucp.common.payment.three_ds_challenge") - """ - A 3DS challenge Action. - """ - - -class Checkout(Checkout_1): - model_config = ConfigDict( - extra="allow", - ) - actions: Actions | None = None diff --git a/src/ucp_sdk/models/schemas/common/payment_split_payments.py b/src/ucp_sdk/models/schemas/common/payment_split_payments.py deleted file mode 100644 index 3551041..0000000 --- a/src/ucp_sdk/models/schemas/common/payment_split_payments.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated, Any - -from pydantic import BaseModel, ConfigDict, Field -from typing_extensions import TypeAliasType - -from ..shopping.checkout import Checkout as Checkout_1 -from .types import amount as amount_1 -from .types import instrument_group -from .types.payment_instrument import PaymentInstrument as PaymentInstrument_1 - -PaymentSplitPaymentsExtension = TypeAliasType( - "PaymentSplitPaymentsExtension", - Annotated[Any, Field(..., title="Payment Split Payments Extension")], -) -""" -Enables Buyers to use multiple payment instruments for a single session. -""" - - -InstrumentGroup = TypeAliasType( - "InstrumentGroup", instrument_group.InstrumentGroup -) - - -SplitPayments = TypeAliasType("SplitPayments", Any) - - -class PaymentInstrument(PaymentInstrument_1): - """ - Payment instrument extended with an optional per-instrument amount for split payments. - """ - - model_config = ConfigDict( - extra="allow", - ) - amount: amount_1.Amount | None = None - """ - Contribution amount for this instrument expressed in ISO 4217 minor units of the containing capability object's `currency`. On request: the platform's requested contribution (omit for open-amount). On response: the actual amount authorized or charged (omitted when not finally processed). - """ - - -class Payment(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - instruments: list[PaymentInstrument] | None = None - """ - Payment instruments in allocation priority order. - """ - - -class Checkout(Checkout_1): - """ - Checkout extended with split payment instrument amounts. - """ - - model_config = ConfigDict( - extra="allow", - ) - payment: Payment | None = None diff --git a/src/ucp_sdk/models/schemas/common/payment_terms.py b/src/ucp_sdk/models/schemas/common/payment_terms.py deleted file mode 100644 index 1e2d6ee..0000000 --- a/src/ucp_sdk/models/schemas/common/payment_terms.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated, Any - -from pydantic import BaseModel, ConfigDict, Field -from typing_extensions import TypeAliasType - -from ..shopping.checkout import Checkout as Checkout_1 -from ..shopping.order import Order as Order_1 -from .types import payment_term - -PaymentTermsExtension = TypeAliasType( - "PaymentTermsExtension", - Annotated[Any, Field(..., title="Payment Terms Extension")], -) -""" -Extends capabilities (e.g., checkout in retail shopping) with selectable payment terms, letting a Business offer alternative schedules for when payment for the checkout is due, and projects the accepted term onto the resulting Order. -""" - - -PaymentTerm = TypeAliasType("PaymentTerm", payment_term.PaymentTerm) - - -class Payment(BaseModel): - """ - Payment object extended with selectable payment terms. - """ - - model_config = ConfigDict( - extra="allow", - ) - terms: list[PaymentTerm] | None = Field(None, min_length=1) - """ - Payment terms the Buyer can choose from. An unselected term's amounts are indicative; the selected term's schedule amounts sum to the checkout total. - """ - selected_term_id: str | None = None - """ - ID of the selected payment term. MUST match one `terms[].id` from the latest Checkout response. Present in a response whenever `terms` is, and absent when it is not: the Checkout total is the selected term's total, so a list of terms without a selection would show an amount that matches no stated term. Where the Buyer has made no choice, the Business selects a default. Omitted on create requests because term IDs are checkout-scoped and no terms exist yet, and on complete requests because the term is already agreed by then. Selecting a term is an Update Checkout mutation: the Business response is authoritative for all derived state. - """ - - -class OrderPayment(BaseModel): - """ - Order payment details carrying the term the Buyer accepted at checkout. - """ - - model_config = ConfigDict( - extra="allow", - ) - accepted_term: PaymentTerm | None = None - """ - The payment term the Buyer accepted at checkout. Businesses MUST carry it forward so the Order states the amounts owed and when, and MUST ensure its schedule amounts sum to the Order total. The available terms are checkout state and are not projected. - """ - - -class Checkout(Checkout_1): - """ - Checkout extended with selectable payment terms. - """ - - model_config = ConfigDict( - extra="allow", - ) - payment: Payment | None = None - """ - Payment details with available and selected payment terms. - """ - - -class Order(Order_1): - """ - Order extended with the payment term accepted at checkout. - """ - - model_config = ConfigDict( - extra="allow", - ) - payment: OrderPayment | None = None - """ - Payment details for the Order, including the accepted payment term. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/__init__.py b/src/ucp_sdk/models/schemas/common/types/__init__.py index 1252d6b..b75a947 100644 --- a/src/ucp_sdk/models/schemas/common/types/__init__.py +++ b/src/ucp_sdk/models/schemas/common/types/__init__.py @@ -1,17 +1,17 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +"""Common types.""" -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +from .totals import * # noqa: F403 +from .amount import * # noqa: F403 +from .unit import * # noqa: F403 +from .signals import * # noqa: F403 +from .description import * # noqa: F403 +from .card_payment_instrument import * # noqa: F403 +from .payment_instrument import * # noqa: F403 +from .error_response import * # noqa: F403 +from .postal_address import * # noqa: F403 +from .payment import * # noqa: F403 +from .message import * # noqa: F403 +from .locality import * # noqa: F403 +from .context import * # noqa: F403 +from .policy import * # noqa: F403 +from .actions import * # noqa: F403 diff --git a/src/ucp_sdk/models/schemas/common/types/actions.py b/src/ucp_sdk/models/schemas/common/types/actions.py index d67b151..6578f53 100644 --- a/src/ucp_sdk/models/schemas/common/types/actions.py +++ b/src/ucp_sdk/models/schemas/common/types/actions.py @@ -1,56 +1,7 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Actions models.""" from __future__ import annotations -from typing import Annotated, Any - -from pydantic import BaseModel, ConfigDict, Field -from typing_extensions import TypeAliasType - -from . import reverse_domain_name - - -class Instance(BaseModel): - """ - Common fields for one outstanding Action instance are id and optional config. The extension declaring the Action type defines type-specific processing data under config. Additional properties are permitted for forward compatibility. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str = Field(..., min_length=1) - """ - Identifier for this Action instance. - """ - config: dict[str, Any] | None = None - """ - Configuration defined by the extension that declares this Action type. - """ - +from ...models import Actions -Actions = TypeAliasType( - "Actions", - Annotated[ - dict[reverse_domain_name.ReverseDomainName, list[Instance]], - Field(..., title="Actions"), - ], -) -""" -Outstanding extension-defined Action instances, keyed by reverse-domain Action type, not extension name. -""" +__all__ = ["Actions"] diff --git a/src/ucp_sdk/models/schemas/common/types/amenity_type.py b/src/ucp_sdk/models/schemas/common/types/amenity_type.py deleted file mode 100644 index cdf4b55..0000000 --- a/src/ucp_sdk/models/schemas/common/types/amenity_type.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated - -from pydantic import Field -from typing_extensions import TypeAliasType - -from . import reverse_domain_name - -AmenityType = TypeAliasType( - "AmenityType", - Annotated[ - reverse_domain_name.ReverseDomainName, - Field( - ..., - examples=[ - "dev.ucp.amenity.wi_fi", - "dev.ucp.amenity.parking", - "dev.ucp.amenity.shopping.in_store_pickup", - "dev.ucp.amenity.shopping.curbside_pickup", - ], - title="Amenity Type", - ), - ], -) -""" -A standardized open reverse-DNS string representing a physical feature, capability, or service provided by a location. Supports a 2-tier hierarchy when defining well-known values in UCP: 1) Common vocabulary for universal features that directly live on `dev.ucp.amenity` (e.g., dev.ucp.amenity.wi_fi) and 2) Industry-scoped features that MUST append their related service name on top of `dev.ucp.amenity` (e.g., dev.ucp.amenity.shopping.in_store_pickup). Businesses MAY define custom vocabulary in their own domain (e.g., com.example.amenity.auto_care_center). -""" diff --git a/src/ucp_sdk/models/schemas/common/types/amount.py b/src/ucp_sdk/models/schemas/common/types/amount.py index f4d901d..abef42c 100644 --- a/src/ucp_sdk/models/schemas/common/types/amount.py +++ b/src/ucp_sdk/models/schemas/common/types/amount.py @@ -1,32 +1,7 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Amount and price models.""" from __future__ import annotations -from typing import Annotated - -from pydantic import Field -from typing_extensions import TypeAliasType +from ...models import Amount, Price, SignedAmount -Amount = TypeAliasType( - "Amount", - Annotated[int, Field(..., ge=0, le=9007199254740991, title="Amount")], -) -""" -Monetary amount in the currency's minor unit as defined by ISO 4217. Refer to the currency's exponent to determine minor-to-major ratio (e.g., 2 for USD, 0 for JPY, 3 for KWD). -""" +__all__ = ["Amount", "Price", "SignedAmount"] diff --git a/src/ucp_sdk/models/schemas/common/types/available_payment_instrument.py b/src/ucp_sdk/models/schemas/common/types/available_payment_instrument.py deleted file mode 100644 index 237df44..0000000 --- a/src/ucp_sdk/models/schemas/common/types/available_payment_instrument.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - -from . import constraint_expression - - -class AvailablePaymentInstrument(BaseModel): - """ - An instrument type available from a payment handler with optional constraints. - """ - - model_config = ConfigDict( - extra="allow", - ) - type: str - """ - The instrument type identifier (e.g., 'card', 'gift_card'). References an instrument schema's type constant. - """ - constraints: constraint_expression.ConstraintExpression | None = None - """ - A Constraint Expression describing the instrument this entry makes available. Keys in `properties` name members of the `constraint_target` declared by the instrument schema for this `type`. Requirements on submitted request data belong in `ucp.request_constraints` instead. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/binding.py b/src/ucp_sdk/models/schemas/common/types/binding.py deleted file mode 100644 index 021401e..0000000 --- a/src/ucp_sdk/models/schemas/common/types/binding.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field - -from . import reverse_domain_name - - -class Binding(BaseModel): - """ - Binds a credential or token to a specific capability resource. Prevents reuse across different resources. - """ - - model_config = ConfigDict( - extra="allow", - ) - type: reverse_domain_name.ReverseDomainName - """ - The capability that owns the bound resource, for example dev.ucp.shopping.checkout. MUST be a capability name declared in the UCP namespace. - """ - id: str = Field(..., min_length=1) - """ - Opaque identifier of the bound resource within the owning capability, for example a checkout identifier. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/business_split_payments_config.py b/src/ucp_sdk/models/schemas/common/types/business_split_payments_config.py deleted file mode 100644 index 0f87555..0000000 --- a/src/ucp_sdk/models/schemas/common/types/business_split_payments_config.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated - -from pydantic import BaseModel, ConfigDict, Field -from typing_extensions import TypeAliasType - -from . import instrument_group - -AllowedCombination = TypeAliasType( - "AllowedCombination", - Annotated[list[instrument_group.InstrumentGroup], Field(..., min_length=1)], -) -""" -A single valid combination: an array of instrument groups that together define the constraints. All groups must be satisfied (AND logic). -""" - - -class BusinessSplitPaymentsConfig(BaseModel): - """ - Business-level configuration for split payments. Declaring the capability means multiple payment instruments are supported; this config declares which combinations are valid. - """ - - model_config = ConfigDict( - extra="allow", - ) - allowed_combinations: list[AllowedCombination] = Field(..., min_length=1) - """ - Array of valid instrument combinations. Each combination is an array of instrument groups. A payment is valid if it matches any combination. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/card_credential.py b/src/ucp_sdk/models/schemas/common/types/card_credential.py deleted file mode 100644 index b7f671a..0000000 --- a/src/ucp_sdk/models/schemas/common/types/card_credential.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Literal - -from pydantic import ConfigDict, Field - -from .payment_credential import PaymentCredential - - -class CardCredential(PaymentCredential): - """ - Deprecated: use PAN Credential (`pan_credential.json`) or Network Token Credential (`network_token_credential.json`). A card credential containing sensitive payment card details including raw Primary Account Numbers (PANs). This credential type MUST NOT be used for checkout, only with payment handlers that tokenize or encrypt credentials. CRITICAL: Both parties handling CardCredential (sender and receiver) MUST be PCI DSS compliant. Transmission MUST use HTTPS/TLS with strong cipher suites. - """ - - model_config = ConfigDict( - extra="allow", - ) - type: Literal["card"] - """ - The credential type identifier for card credentials. - """ - card_number_type: Literal["fpan", "network_token", "dpan"] - """ - Deprecated: the credential type now carries this distinction. The type of card number. Network tokens are preferred with fallback to FPAN. See PCI Scope for more details. - """ - number: str | None = Field(None, examples=["4242424242424242"]) - """ - Card number. - """ - expiry_month: int | None = None - """ - The month of the card's expiration date (1-12). - """ - expiry_year: int | None = None - """ - The year of the card's expiration date. - """ - name: str | None = Field(None, examples=["Jane Doe"]) - """ - Cardholder name. - """ - cvc: str | None = Field(None, examples=["223"], max_length=4) - """ - Card CVC number. - """ - cryptogram: str | None = Field(None, examples=["gXc5UCLnM6ckD7pjM1TdPA=="]) - """ - Cryptogram provided with network tokens. - """ - eci_value: str | None = Field(None, examples=["07"]) - """ - Electronic Commerce Indicator / Security Level Indicator provided with network tokens. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/card_payment_instrument.py b/src/ucp_sdk/models/schemas/common/types/card_payment_instrument.py index ea5ba62..f072c1c 100644 --- a/src/ucp_sdk/models/schemas/common/types/card_payment_instrument.py +++ b/src/ucp_sdk/models/schemas/common/types/card_payment_instrument.py @@ -1,97 +1,13 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Card payment instrument models.""" from __future__ import annotations -from typing import Literal - -from pydantic import AnyUrl, BaseModel, ConfigDict, Field - -from .payment_instrument import PaymentInstrument - - -class Display(BaseModel): - """ - Display information for this card payment instrument. - """ - - model_config = ConfigDict( - extra="allow", - ) - brand: str | None = None - """ - The card brand/network shown to the buyer (e.g., visa, mastercard, amex). Presentational only. - """ - last_digits: str | None = None - """ - Last 4 digits of the card number. - """ - expiry_month: int | None = None - """ - The month of the card's expiration date (1-12). - """ - expiry_year: int | None = None - """ - The year of the card's expiration date. - """ - description: str | None = None - """ - An optional rich text description of the card to display to the user (e.g., 'Visa ending in 1234, expires 12/2025'). - """ - card_art: AnyUrl | None = None - """ - An optional URI to a rich image representing the card (e.g., card art provided by the issuer). - """ - - -class ConstraintTarget(BaseModel): - """ - The object an available card instrument's `constraints` describes. It declares the constrainable members and their types and is never carried in a payload. - """ - - model_config = ConfigDict( - extra="allow", - ) - brand: str | None = Field( - None, examples=["visa", "mastercard", "cartebancaire"] - ) - """ - Card scheme. Derived from the account number, not submitted. - """ - +from ...models import AvailablePaymentInstrument, PaymentInstrument -class CardPaymentInstrument(PaymentInstrument): - """ - A basic card payment instrument with visible card details. Can be inherited by a handler's instrument schema to define handler-specific display details or more complex credential structures. - """ +CardPaymentInstrument = PaymentInstrument - model_config = ConfigDict( - extra="allow", - ) - type: Literal["card"] - """ - Indicates this is a card payment instrument. - """ - network: str | None = None - """ - Card network elected for this transaction, typically a co-badged selection. When present, the business MAY decline if the card cannot route over it and MUST NOT substitute another. - """ - display: Display | None = None - """ - Display information for this card payment instrument. - """ +__all__ = [ + "CardPaymentInstrument", + "AvailablePaymentInstrument", + "PaymentInstrument", +] diff --git a/src/ucp_sdk/models/schemas/common/types/constraint_expression.py b/src/ucp_sdk/models/schemas/common/types/constraint_expression.py deleted file mode 100644 index 236ae50..0000000 --- a/src/ucp_sdk/models/schemas/common/types/constraint_expression.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Any - -from pydantic import BaseModel, ConfigDict, Field, field_validator -from typing_extensions import TypeAliasType - - -class ValueConstraint1(BaseModel): - """ - A Value Constraint containing `enum`, `const`, or both. - """ - - model_config = ConfigDict( - extra="allow", - ) - enum: list[Any] = Field(..., min_length=1) - """ - A non-empty array of unique JSON values. - """ - const: Any | None = None - - -class ValueConstraint2(BaseModel): - """ - A Value Constraint containing `enum`, `const`, or both. - """ - - model_config = ConfigDict( - extra="allow", - ) - enum: list[Any] | None = Field(None, min_length=1) - """ - A non-empty array of unique JSON values. - """ - const: Any - - -ValueConstraint = TypeAliasType( - "ValueConstraint", ValueConstraint1 | ValueConstraint2 -) -""" -A Value Constraint containing `enum`, `const`, or both. -""" - - -class ConstraintExpression(BaseModel): - """ - A closed JSON Schema Draft 2020-12 constraint expression with Object and Value Constraint positions. - """ - - model_config = ConfigDict( - extra="forbid", - ) - required: list[str] | None = Field(None, min_length=1) - """ - Property names required by the constrained object. Must be non-empty: an empty array applies no constraint. - """ - properties: dict[str, ConstraintExpression | ValueConstraint] | None = None - """ - Constraints keyed by property name. Must be non-empty: an empty object applies no constraint. - """ - anyOf: list[ConstraintExpression] | None = None - """ - Alternative Object Constraints. The constrained object must satisfy at least one. A branch must be non-empty: an empty branch is satisfied by every object and neutralizes the alternation. - """ - - @field_validator("required", mode="after") - def _enforce_unique_items_required(cls, value): # noqa: N805 - """JSON Schema uniqueItems: reject duplicate entries.""" - if value is None: - return value - seen = [] - for item in value: - if item in seen: - raise ValueError( - "Items must be unique (schema uniqueItems=true)" - ) - seen.append(item) - return value - - -ConstraintExpression.model_rebuild() diff --git a/src/ucp_sdk/models/schemas/common/types/context.py b/src/ucp_sdk/models/schemas/common/types/context.py index b4bb158..da2571f 100644 --- a/src/ucp_sdk/models/schemas/common/types/context.py +++ b/src/ucp_sdk/models/schemas/common/types/context.py @@ -1,86 +1,7 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Context models.""" from __future__ import annotations -from pydantic import BaseModel, ConfigDict, field_validator - -from . import reverse_domain_name -from .locality import Locality - - -class PaymentItem(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - handler: reverse_domain_name.ReverseDomainName - """ - Handler registry key advertised in the Business profile's `ucp.payment_handlers`. - """ - types: list[str] | None = None - """ - Optional preferred instrument types for this handler, in priority order, aligned with the handler's advertised `payment_instrument.type` values (for example `card` or `bank`). Unrecognized values MUST be ignored. - """ - - -class Context(Locality): - """ - Provisional buyer signals for relevance and localization—not authoritative data. Businesses SHOULD use these values when verified inputs (e.g., shipping address) are absent, and MAY ignore or down-rank them if inconsistent with higher-confidence signals (authenticated account, risk detection) or regulatory constraints (export controls). Eligibility and policy enforcement MUST occur at checkout time using binding transaction data. Context SHOULD be non-identifying and can be disclosed progressively—coarse signals early, finer resolution as the session progresses. Higher-resolution data (shipping address, billing address) supersedes context. - """ - - model_config = ConfigDict( - extra="allow", - ) - location: str | None = None - """ - Stable, opaque identifier for a Location in the Business's namespace. This provisional, non-binding hint is distinct from the Buyer's locality. The operation specification or an active capability/extension defines its effects. A common example in retail shopping is the default home store ID selected and saved by the user when purchasing groceries. - """ - intent: str | None = None - """ - Background context describing buyer's intent (e.g., 'looking for a gift under $50', 'need something durable for outdoor use'). Informs relevance, recommendations, and personalization. - """ - language: str | None = None - """ - Preferred language for content. Use IETF BCP 47 language tags (e.g., 'en', 'fr-CA', 'zh-Hans'). For REST, equivalent to Accept-Language header—platforms SHOULD fall back to Accept-Language when this field is absent; when provided, overrides Accept-Language. Businesses MAY return content in a different language if unavailable. - """ - currency: str | None = None - """ - Preferred currency (ISO 4217, e.g., 'EUR', 'USD'). Businesses determine presentment currency from context and authoritative signals; this hint MAY inform selection in multi-currency markets. Also serves as the denomination for price filter values — platforms SHOULD include this field when sending price filters. Response prices include explicit currency confirming the resolution. - """ - eligibility: list[reverse_domain_name.ReverseDomainName] | None = None - """ - Buyer claims about eligible benefits such as loyalty membership, payment instrument perks, and similar. Recognized claims MAY inform the Business response (e.g., member-only product availability, adjusted pricing in catalog, provisional discounts at cart or checkout). Businesses MUST ignore unrecognized values without error. Values MUST use reverse-domain naming (e.g., 'com.example.loyalty_gold', 'org.school.student') and MUST be non-identifying. - """ - payment: list[PaymentItem] | None = None - """ - Buyer-preferred payment handlers in priority order (most preferred first). Each entry names a handler advertised in the Business profile's `ucp.payment_handlers`, optionally narrowed to preferred instrument types. The Business SHOULD use it to preselect or prioritize the handler (and type, when given) and MAY ignore unavailable or ineligible entries; unrecognized values MUST be ignored without error. - """ +from ...models import Context - @field_validator("eligibility", mode="after") - def _enforce_unique_items_eligibility(cls, value): # noqa: N805 - """JSON Schema uniqueItems: reject duplicate entries.""" - if value is None: - return value - seen = [] - for item in value: - if item in seen: - raise ValueError( - "Items must be unique (schema uniqueItems=true)" - ) - seen.append(item) - return value +__all__ = ["Context"] diff --git a/src/ucp_sdk/models/schemas/common/types/context_create_request.py b/src/ucp_sdk/models/schemas/common/types/context_create_request.py deleted file mode 100644 index 6efdcb9..0000000 --- a/src/ucp_sdk/models/schemas/common/types/context_create_request.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - -from . import reverse_domain_name_create_request -from .locality_create_request import LocalityCreateRequest - - -class PaymentItem(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - handler: reverse_domain_name_create_request.ReverseDomainNameCreateRequest - """ - Handler registry key advertised in the Business profile's `ucp.payment_handlers`. - """ - types: list[str] | None = None - """ - Optional preferred instrument types for this handler, in priority order, aligned with the handler's advertised `payment_instrument.type` values (for example `card` or `bank`). Unrecognized values MUST be ignored. - """ - - -class ContextCreateRequest(LocalityCreateRequest): - """ - Provisional buyer signals for relevance and localization—not authoritative data. Businesses SHOULD use these values when verified inputs (e.g., shipping address) are absent, and MAY ignore or down-rank them if inconsistent with higher-confidence signals (authenticated account, risk detection) or regulatory constraints (export controls). Eligibility and policy enforcement MUST occur at checkout time using binding transaction data. Context SHOULD be non-identifying and can be disclosed progressively—coarse signals early, finer resolution as the session progresses. Higher-resolution data (shipping address, billing address) supersedes context. - """ - - model_config = ConfigDict( - extra="allow", - ) - location: str | None = None - """ - Stable, opaque identifier for a Location in the Business's namespace. This provisional, non-binding hint is distinct from the Buyer's locality. The operation specification or an active capability/extension defines its effects. A common example in retail shopping is the default home store ID selected and saved by the user when purchasing groceries. - """ - intent: str | None = None - """ - Background context describing buyer's intent (e.g., 'looking for a gift under $50', 'need something durable for outdoor use'). Informs relevance, recommendations, and personalization. - """ - language: str | None = None - """ - Preferred language for content. Use IETF BCP 47 language tags (e.g., 'en', 'fr-CA', 'zh-Hans'). For REST, equivalent to Accept-Language header—platforms SHOULD fall back to Accept-Language when this field is absent; when provided, overrides Accept-Language. Businesses MAY return content in a different language if unavailable. - """ - currency: str | None = None - """ - Preferred currency (ISO 4217, e.g., 'EUR', 'USD'). Businesses determine presentment currency from context and authoritative signals; this hint MAY inform selection in multi-currency markets. Also serves as the denomination for price filter values — platforms SHOULD include this field when sending price filters. Response prices include explicit currency confirming the resolution. - """ - eligibility: ( - list[reverse_domain_name_create_request.ReverseDomainNameCreateRequest] - | None - ) = None - """ - Buyer claims about eligible benefits such as loyalty membership, payment instrument perks, and similar. Recognized claims MAY inform the Business response (e.g., member-only product availability, adjusted pricing in catalog, provisional discounts at cart or checkout). Businesses MUST ignore unrecognized values without error. Values MUST use reverse-domain naming (e.g., 'com.example.loyalty_gold', 'org.school.student') and MUST be non-identifying. - """ - payment: list[PaymentItem] | None = None - """ - Buyer-preferred payment handlers in priority order (most preferred first). Each entry names a handler advertised in the Business profile's `ucp.payment_handlers`, optionally narrowed to preferred instrument types. The Business SHOULD use it to preselect or prioritize the handler (and type, when given) and MAY ignore unavailable or ineligible entries; unrecognized values MUST be ignored without error. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/context_update_request.py b/src/ucp_sdk/models/schemas/common/types/context_update_request.py deleted file mode 100644 index b0bd4e1..0000000 --- a/src/ucp_sdk/models/schemas/common/types/context_update_request.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - -from . import reverse_domain_name_update_request -from .locality_update_request import LocalityUpdateRequest - - -class PaymentItem(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - handler: reverse_domain_name_update_request.ReverseDomainNameUpdateRequest - """ - Handler registry key advertised in the Business profile's `ucp.payment_handlers`. - """ - types: list[str] | None = None - """ - Optional preferred instrument types for this handler, in priority order, aligned with the handler's advertised `payment_instrument.type` values (for example `card` or `bank`). Unrecognized values MUST be ignored. - """ - - -class ContextUpdateRequest(LocalityUpdateRequest): - """ - Provisional buyer signals for relevance and localization—not authoritative data. Businesses SHOULD use these values when verified inputs (e.g., shipping address) are absent, and MAY ignore or down-rank them if inconsistent with higher-confidence signals (authenticated account, risk detection) or regulatory constraints (export controls). Eligibility and policy enforcement MUST occur at checkout time using binding transaction data. Context SHOULD be non-identifying and can be disclosed progressively—coarse signals early, finer resolution as the session progresses. Higher-resolution data (shipping address, billing address) supersedes context. - """ - - model_config = ConfigDict( - extra="allow", - ) - location: str | None = None - """ - Stable, opaque identifier for a Location in the Business's namespace. This provisional, non-binding hint is distinct from the Buyer's locality. The operation specification or an active capability/extension defines its effects. A common example in retail shopping is the default home store ID selected and saved by the user when purchasing groceries. - """ - intent: str | None = None - """ - Background context describing buyer's intent (e.g., 'looking for a gift under $50', 'need something durable for outdoor use'). Informs relevance, recommendations, and personalization. - """ - language: str | None = None - """ - Preferred language for content. Use IETF BCP 47 language tags (e.g., 'en', 'fr-CA', 'zh-Hans'). For REST, equivalent to Accept-Language header—platforms SHOULD fall back to Accept-Language when this field is absent; when provided, overrides Accept-Language. Businesses MAY return content in a different language if unavailable. - """ - currency: str | None = None - """ - Preferred currency (ISO 4217, e.g., 'EUR', 'USD'). Businesses determine presentment currency from context and authoritative signals; this hint MAY inform selection in multi-currency markets. Also serves as the denomination for price filter values — platforms SHOULD include this field when sending price filters. Response prices include explicit currency confirming the resolution. - """ - eligibility: ( - list[reverse_domain_name_update_request.ReverseDomainNameUpdateRequest] - | None - ) = None - """ - Buyer claims about eligible benefits such as loyalty membership, payment instrument perks, and similar. Recognized claims MAY inform the Business response (e.g., member-only product availability, adjusted pricing in catalog, provisional discounts at cart or checkout). Businesses MUST ignore unrecognized values without error. Values MUST use reverse-domain naming (e.g., 'com.example.loyalty_gold', 'org.school.student') and MUST be non-identifying. - """ - payment: list[PaymentItem] | None = None - """ - Buyer-preferred payment handlers in priority order (most preferred first). Each entry names a handler advertised in the Business profile's `ucp.payment_handlers`, optionally narrowed to preferred instrument types. The Business SHOULD use it to preselect or prioritize the handler (and type, when given) and MAY ignore unavailable or ineligible entries; unrecognized values MUST be ignored without error. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/daily_hour.py b/src/ucp_sdk/models/schemas/common/types/daily_hour.py deleted file mode 100644 index 15aae94..0000000 --- a/src/ucp_sdk/models/schemas/common/types/daily_hour.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Any, Literal - -from pydantic import ConfigDict - -from .time_interval import TimeInterval - - -class DailyHour(TimeInterval): - """ - A regular weekly operating interval. Its `day`, `opens`, and `closes` are recurring local civil values interpreted in the containing Location's `timezone`. Multiple entries for the same day support split shifts. - """ - - model_config = ConfigDict( - extra="allow", - ) - day: Literal[ - "monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - "sunday", - ] - """ - A stable UCP day-of-week identifier for the day on which this recurring local civil-time interval begins in the containing Location's `timezone`. It is not localized display text. - """ - opens: Any - closes: Any diff --git a/src/ucp_sdk/models/schemas/common/types/daily_hour_create_request.py b/src/ucp_sdk/models/schemas/common/types/daily_hour_create_request.py deleted file mode 100644 index 4dc0904..0000000 --- a/src/ucp_sdk/models/schemas/common/types/daily_hour_create_request.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import ConfigDict - -from .time_interval_create_request import TimeIntervalCreateRequest - - -class DailyHourCreateRequest(TimeIntervalCreateRequest): - """ - A regular weekly operating interval. Its `day`, `opens`, and `closes` are recurring local civil values interpreted in the containing Location's `timezone`. Multiple entries for the same day support split shifts. - """ - - model_config = ConfigDict( - extra="allow", - ) diff --git a/src/ucp_sdk/models/schemas/common/types/daily_hour_update_request.py b/src/ucp_sdk/models/schemas/common/types/daily_hour_update_request.py deleted file mode 100644 index 08e341f..0000000 --- a/src/ucp_sdk/models/schemas/common/types/daily_hour_update_request.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import ConfigDict - -from .time_interval_update_request import TimeIntervalUpdateRequest - - -class DailyHourUpdateRequest(TimeIntervalUpdateRequest): - """ - A regular weekly operating interval. Its `day`, `opens`, and `closes` are recurring local civil values interpreted in the containing Location's `timezone`. Multiple entries for the same day support split shifts. - """ - - model_config = ConfigDict( - extra="allow", - ) diff --git a/src/ucp_sdk/models/schemas/common/types/description.py b/src/ucp_sdk/models/schemas/common/types/description.py index 4d7fc3b..58dc05f 100644 --- a/src/ucp_sdk/models/schemas/common/types/description.py +++ b/src/ucp_sdk/models/schemas/common/types/description.py @@ -1,54 +1,7 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Description models.""" from __future__ import annotations -from pydantic import BaseModel, ConfigDict, model_validator - - -class Description(BaseModel): - """ - Description content in one or more formats. At least one format must be provided. - """ - - model_config = ConfigDict( - extra="allow", - ) - plain: str | None = None - """ - Plain text content. - """ - html: str | None = None - """ - HTML-formatted content. Security: Platforms MUST sanitize before rendering—strip scripts, event handlers, and untrusted elements. Treat all rich text as untrusted input. - """ - markdown: str | None = None - """ - Markdown-formatted content. - """ +from ...models import Description - @model_validator(mode="after") - def _enforce_min_properties(self): - """JSON Schema minProperties: require at least 1 - provided property.""" - provided = self.model_fields_set | set(self.model_extra or {}) - if len(provided) < 1: - raise ValueError( - "At least 1 property must be provided (schema minProperties=1)" - ) - return self +__all__ = ["Description"] diff --git a/src/ucp_sdk/models/schemas/common/types/error_code.py b/src/ucp_sdk/models/schemas/common/types/error_code.py deleted file mode 100644 index 07e34dd..0000000 --- a/src/ucp_sdk/models/schemas/common/types/error_code.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated - -from pydantic import Field -from typing_extensions import TypeAliasType - -ErrorCode = TypeAliasType( - "ErrorCode", - Annotated[ - str, - Field( - ..., - examples=[ - "not_found", - "out_of_stock", - "item_unavailable", - "address_undeliverable", - "payment_failed", - "eligibility_invalid", - "identity_required", - "insufficient_scope", - ], - title="Error Code", - ), - ], -) -""" -Error code identifying the type of error. Standard errors are defined in capability specifications (see examples) and have standardized semantics; freeform codes are permitted. -""" diff --git a/src/ucp_sdk/models/schemas/common/types/error_code_create_request.py b/src/ucp_sdk/models/schemas/common/types/error_code_create_request.py deleted file mode 100644 index 7ccc827..0000000 --- a/src/ucp_sdk/models/schemas/common/types/error_code_create_request.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated - -from pydantic import Field -from typing_extensions import TypeAliasType - -ErrorCodeCreateRequest = TypeAliasType( - "ErrorCodeCreateRequest", - Annotated[ - str, - Field( - ..., - examples=[ - "not_found", - "out_of_stock", - "item_unavailable", - "address_undeliverable", - "payment_failed", - "eligibility_invalid", - "identity_required", - "insufficient_scope", - ], - title="Error Code Create Request", - ), - ], -) -""" -Error code identifying the type of error. Standard errors are defined in capability specifications (see examples) and have standardized semantics; freeform codes are permitted. -""" diff --git a/src/ucp_sdk/models/schemas/common/types/error_code_update_request.py b/src/ucp_sdk/models/schemas/common/types/error_code_update_request.py deleted file mode 100644 index ec18e56..0000000 --- a/src/ucp_sdk/models/schemas/common/types/error_code_update_request.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated - -from pydantic import Field -from typing_extensions import TypeAliasType - -ErrorCodeUpdateRequest = TypeAliasType( - "ErrorCodeUpdateRequest", - Annotated[ - str, - Field( - ..., - examples=[ - "not_found", - "out_of_stock", - "item_unavailable", - "address_undeliverable", - "payment_failed", - "eligibility_invalid", - "identity_required", - "insufficient_scope", - ], - title="Error Code Update Request", - ), - ], -) -""" -Error code identifying the type of error. Standard errors are defined in capability specifications (see examples) and have standardized semantics; freeform codes are permitted. -""" diff --git a/src/ucp_sdk/models/schemas/common/types/error_response.py b/src/ucp_sdk/models/schemas/common/types/error_response.py index d4628a0..a295480 100644 --- a/src/ucp_sdk/models/schemas/common/types/error_response.py +++ b/src/ucp_sdk/models/schemas/common/types/error_response.py @@ -1,46 +1,7 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Error response models.""" from __future__ import annotations -from pydantic import AnyUrl, BaseModel, ConfigDict, Field - -from ... import ucp as ucp_1 -from . import message - - -class ErrorResponse(BaseModel): - """ - Generic error response when business logic prevents resource creation or failed to retrieve resource. Used when no valid resource can be established. - """ +from ...models import Error, ErrorCode, ErrorResponse - model_config = ConfigDict( - extra="forbid", - ) - ucp: ucp_1.UcpMetadata - """ - UCP protocol metadata. Status MUST be 'error' for error response. - """ - messages: list[message.Message] = Field(..., min_length=1) - """ - Array of messages describing why the operation failed. - """ - continue_url: AnyUrl | None = None - """ - URL for buyer handoff or session recovery. - """ +__all__ = ["ErrorResponse", "Error", "ErrorCode"] diff --git a/src/ucp_sdk/models/schemas/common/types/exception_hour.py b/src/ucp_sdk/models/schemas/common/types/exception_hour.py deleted file mode 100644 index bed674f..0000000 --- a/src/ucp_sdk/models/schemas/common/types/exception_hour.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from datetime import date - -from pydantic import ConfigDict - -from .time_interval import TimeInterval - - -class ExceptionHour(TimeInterval): - """ - A date-specific operating interval or full closure. Its `valid_from`, `valid_through`, `opens`, and `closes` are local civil values interpreted in the containing Location's `timezone`. Date bounds are inclusive. - """ - - model_config = ConfigDict( - extra="allow", - ) - title: str | None = None - """ - A short human-readable heading naming the exception (for example, 'Thanksgiving'). Presentation metadata that does not affect schedule evaluation. - """ - valid_from: date - """ - The first local civil date to which this exception applies, interpreted in the containing Location's `timezone`. - """ - valid_through: date - """ - The last local civil date to which this exception applies, interpreted in the containing Location's `timezone`. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/exception_hour_create_request.py b/src/ucp_sdk/models/schemas/common/types/exception_hour_create_request.py deleted file mode 100644 index 85ed698..0000000 --- a/src/ucp_sdk/models/schemas/common/types/exception_hour_create_request.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import ConfigDict - -from .time_interval_create_request import TimeIntervalCreateRequest - - -class ExceptionHourCreateRequest(TimeIntervalCreateRequest): - """ - A date-specific operating interval or full closure. Its `valid_from`, `valid_through`, `opens`, and `closes` are local civil values interpreted in the containing Location's `timezone`. Date bounds are inclusive. - """ - - model_config = ConfigDict( - extra="allow", - ) diff --git a/src/ucp_sdk/models/schemas/common/types/exception_hour_update_request.py b/src/ucp_sdk/models/schemas/common/types/exception_hour_update_request.py deleted file mode 100644 index c2f2e11..0000000 --- a/src/ucp_sdk/models/schemas/common/types/exception_hour_update_request.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import ConfigDict - -from .time_interval_update_request import TimeIntervalUpdateRequest - - -class ExceptionHourUpdateRequest(TimeIntervalUpdateRequest): - """ - A date-specific operating interval or full closure. Its `valid_from`, `valid_through`, `opens`, and `closes` are local civil values interpreted in the containing Location's `timezone`. Date bounds are inclusive. - """ - - model_config = ConfigDict( - extra="allow", - ) diff --git a/src/ucp_sdk/models/schemas/common/types/geo.py b/src/ucp_sdk/models/schemas/common/types/geo.py deleted file mode 100644 index a1feb5b..0000000 --- a/src/ucp_sdk/models/schemas/common/types/geo.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field - - -class Geo(BaseModel): - """ - WGS 84 geographic coordinates in decimal degrees. - """ - - model_config = ConfigDict( - extra="allow", - ) - latitude: float = Field(..., ge=-90.0, le=90.0) - """ - WGS 84 latitude in decimal degrees. - """ - longitude: float = Field(..., ge=-180.0, le=180.0) - """ - WGS 84 longitude in decimal degrees. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/geo_create_request.py b/src/ucp_sdk/models/schemas/common/types/geo_create_request.py deleted file mode 100644 index 87ba9e8..0000000 --- a/src/ucp_sdk/models/schemas/common/types/geo_create_request.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field - - -class GeoCreateRequest(BaseModel): - """ - WGS 84 geographic coordinates in decimal degrees. - """ - - model_config = ConfigDict( - extra="allow", - ) - latitude: float = Field(..., ge=-90.0, le=90.0) - """ - WGS 84 latitude in decimal degrees. - """ - longitude: float = Field(..., ge=-180.0, le=180.0) - """ - WGS 84 longitude in decimal degrees. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/geo_update_request.py b/src/ucp_sdk/models/schemas/common/types/geo_update_request.py deleted file mode 100644 index 6aa7dfb..0000000 --- a/src/ucp_sdk/models/schemas/common/types/geo_update_request.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field - - -class GeoUpdateRequest(BaseModel): - """ - WGS 84 geographic coordinates in decimal degrees. - """ - - model_config = ConfigDict( - extra="allow", - ) - latitude: float = Field(..., ge=-90.0, le=90.0) - """ - WGS 84 latitude in decimal degrees. - """ - longitude: float = Field(..., ge=-180.0, le=180.0) - """ - WGS 84 longitude in decimal degrees. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/info_code.py b/src/ucp_sdk/models/schemas/common/types/info_code.py deleted file mode 100644 index e691c86..0000000 --- a/src/ucp_sdk/models/schemas/common/types/info_code.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated - -from pydantic import Field -from typing_extensions import TypeAliasType - -InfoCode = TypeAliasType( - "InfoCode", - Annotated[ - str, - Field( - ..., - examples=[ - "identity_optional", - "signal", - "free_shipping", - "not_found", - ], - title="Info Code", - ), - ], -) -""" -Info code identifying the type of informational message. Standard codes are defined in capability specifications (see examples) and have standardized semantics; freeform codes are permitted. -""" diff --git a/src/ucp_sdk/models/schemas/common/types/info_code_create_request.py b/src/ucp_sdk/models/schemas/common/types/info_code_create_request.py deleted file mode 100644 index ad7cba6..0000000 --- a/src/ucp_sdk/models/schemas/common/types/info_code_create_request.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated - -from pydantic import Field -from typing_extensions import TypeAliasType - -InfoCodeCreateRequest = TypeAliasType( - "InfoCodeCreateRequest", - Annotated[ - str, - Field( - ..., - examples=[ - "identity_optional", - "signal", - "free_shipping", - "not_found", - ], - title="Info Code Create Request", - ), - ], -) -""" -Info code identifying the type of informational message. Standard codes are defined in capability specifications (see examples) and have standardized semantics; freeform codes are permitted. -""" diff --git a/src/ucp_sdk/models/schemas/common/types/info_code_update_request.py b/src/ucp_sdk/models/schemas/common/types/info_code_update_request.py deleted file mode 100644 index 65c287f..0000000 --- a/src/ucp_sdk/models/schemas/common/types/info_code_update_request.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated - -from pydantic import Field -from typing_extensions import TypeAliasType - -InfoCodeUpdateRequest = TypeAliasType( - "InfoCodeUpdateRequest", - Annotated[ - str, - Field( - ..., - examples=[ - "identity_optional", - "signal", - "free_shipping", - "not_found", - ], - title="Info Code Update Request", - ), - ], -) -""" -Info code identifying the type of informational message. Standard codes are defined in capability specifications (see examples) and have standardized semantics; freeform codes are permitted. -""" diff --git a/src/ucp_sdk/models/schemas/common/types/instrument_group.py b/src/ucp_sdk/models/schemas/common/types/instrument_group.py deleted file mode 100644 index 55bb991..0000000 --- a/src/ucp_sdk/models/schemas/common/types/instrument_group.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field - - -class InstrumentGroup(BaseModel): - """ - A constraint within an allowed combination that defines which instrument types can fill this group and how many are permitted. - """ - - model_config = ConfigDict( - extra="allow", - ) - types: list[str] = Field(..., min_length=1) - """ - Instrument types accepted by this group (OR logic). Any listed type qualifies. - """ - min: int | None = Field(0, ge=0) - """ - Minimum number of instruments required from this group. Defaults to 0 (optional). - """ - max: int | None = Field(1, ge=1) - """ - Maximum number of instruments allowed from this group. Defaults to 1. MUST be greater than or equal to `min`. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/link.py b/src/ucp_sdk/models/schemas/common/types/link.py deleted file mode 100644 index c3cdd02..0000000 --- a/src/ucp_sdk/models/schemas/common/types/link.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import AnyUrl, BaseModel, ConfigDict - - -class Link(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - type: str - """ - Type of link. Well-known values: `privacy_policy`, `terms_of_service`, `refund_policy`, `shipping_policy`, `faq`. Consumers SHOULD handle unknown values gracefully by displaying them using the `title` field or omitting the link. - """ - url: AnyUrl - """ - The actual URL pointing to the content to be displayed. - """ - title: str | None = None - """ - Optional display text for the link. When provided, use this instead of generating from type. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/locality.py b/src/ucp_sdk/models/schemas/common/types/locality.py index 35ff9b8..1dc28ac 100644 --- a/src/ucp_sdk/models/schemas/common/types/locality.py +++ b/src/ucp_sdk/models/schemas/common/types/locality.py @@ -1,43 +1,7 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Locality models.""" from __future__ import annotations -from pydantic import BaseModel, ConfigDict - - -class Locality(BaseModel): - """ - A coarse geographic location — country, region, and postal code. A lightweight alternative to a full postal address. - """ +from ...models import Locality - model_config = ConfigDict( - extra="allow", - ) - address_country: str | None = None - """ - The country, as a 2-letter ISO 3166-1 alpha-2 code (e.g. "US"). A 3-letter alpha-3 code or full country name MAY also be used. - """ - address_region: str | None = None - """ - The first-level administrative region within the country (e.g. a state or province such as California). - """ - postal_code: str | None = None - """ - The postal code (e.g. "94043"). - """ +__all__ = ["Locality"] diff --git a/src/ucp_sdk/models/schemas/common/types/locality_create_request.py b/src/ucp_sdk/models/schemas/common/types/locality_create_request.py deleted file mode 100644 index 2b6c4c6..0000000 --- a/src/ucp_sdk/models/schemas/common/types/locality_create_request.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class LocalityCreateRequest(BaseModel): - """ - A coarse geographic location — country, region, and postal code. A lightweight alternative to a full postal address. - """ - - model_config = ConfigDict( - extra="allow", - ) - address_country: str | None = None - """ - The country, as a 2-letter ISO 3166-1 alpha-2 code (e.g. "US"). A 3-letter alpha-3 code or full country name MAY also be used. - """ - address_region: str | None = None - """ - The first-level administrative region within the country (e.g. a state or province such as California). - """ - postal_code: str | None = None - """ - The postal code (e.g. "94043"). - """ diff --git a/src/ucp_sdk/models/schemas/common/types/locality_update_request.py b/src/ucp_sdk/models/schemas/common/types/locality_update_request.py deleted file mode 100644 index 26c74ad..0000000 --- a/src/ucp_sdk/models/schemas/common/types/locality_update_request.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class LocalityUpdateRequest(BaseModel): - """ - A coarse geographic location — country, region, and postal code. A lightweight alternative to a full postal address. - """ - - model_config = ConfigDict( - extra="allow", - ) - address_country: str | None = None - """ - The country, as a 2-letter ISO 3166-1 alpha-2 code (e.g. "US"). A 3-letter alpha-3 code or full country name MAY also be used. - """ - address_region: str | None = None - """ - The first-level administrative region within the country (e.g. a state or province such as California). - """ - postal_code: str | None = None - """ - The postal code (e.g. "94043"). - """ diff --git a/src/ucp_sdk/models/schemas/common/types/location.py b/src/ucp_sdk/models/schemas/common/types/location.py deleted file mode 100644 index abf19c9..0000000 --- a/src/ucp_sdk/models/schemas/common/types/location.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field - -from . import amenity_type, daily_hour, exception_hour -from . import geo as geo_1 -from .location_summary import LocationSummary - - -class Amenity(BaseModel): - """ - Buyer-facing presentation metadata for one amenity identifier. The containing map key, not this metadata, defines amenity identity and filter matching. - """ - - model_config = ConfigDict( - extra="allow", - ) - description: str = Field(..., min_length=1) - """ - Short, plain-text, buyer-facing label or phrase for the amenity, suitable for direct use in a compact list (e.g., 'Curbside pickup'). The Business SHOULD localize it for the request when possible. This content does not participate in amenity identity or filter matching. - """ - - -class Location(LocationSummary): - """ - The full, rich representation of a physical business location. Builds on the Location Summary schema with discovery-centric details such as geographic coordinates, operating hours, timezone, and amenities. - """ - - model_config = ConfigDict( - extra="allow", - ) - geo: geo_1.Geo | None = None - """ - Geographic coordinates for the location. - """ - amenities: dict[amenity_type.AmenityType, Amenity] | None = None - """ - Static features, services, or capabilities of the Location, keyed by reverse-domain amenity identifier. Each value provides a buyer-facing description; the key alone defines amenity identity and filter matching. - """ - hours: list[daily_hour.DailyHour] | None = None - """ - Regular weekly operating hours whose day and time values use this Location's canonical local civil-time frame. Multiple entries for the same day support split shifts. An omitted day has no regular interval beginning that day; an interval beginning on the preceding day can carry into it. Omission of the entire `hours` property means the regular schedule is unknown. - """ - exception_hours: list[exception_hour.ExceptionHour] | None = None - """ - Date-specific operating-hour exceptions, including full closures, whose date and time values use this Location's canonical local civil-time frame. - """ - timezone: str | None = None - """ - The Business-owned IANA Time Zone Database identifier (e.g., 'America/New_York') defining this Location's canonical local civil-time frame for all returned schedule day, time, and date fields. The Business does not vary this canonical framing by the requesting Platform's or Buyer's timezone. Required when hours or exception_hours is present. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/location_create_request.py b/src/ucp_sdk/models/schemas/common/types/location_create_request.py deleted file mode 100644 index 0e1ef77..0000000 --- a/src/ucp_sdk/models/schemas/common/types/location_create_request.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field - -from .location_summary_create_request import LocationSummaryCreateRequest - - -class Amenity(BaseModel): - """ - Buyer-facing presentation metadata for one amenity identifier. The containing map key, not this metadata, defines amenity identity and filter matching. - """ - - model_config = ConfigDict( - extra="allow", - ) - description: str = Field(..., min_length=1) - """ - Short, plain-text, buyer-facing label or phrase for the amenity, suitable for direct use in a compact list (e.g., 'Curbside pickup'). The Business SHOULD localize it for the request when possible. This content does not participate in amenity identity or filter matching. - """ - - -class LocationCreateRequest(LocationSummaryCreateRequest): - """ - The full, rich representation of a physical business location. Builds on the Location Summary schema with discovery-centric details such as geographic coordinates, operating hours, timezone, and amenities. - """ - - model_config = ConfigDict( - extra="allow", - ) diff --git a/src/ucp_sdk/models/schemas/common/types/location_distance.py b/src/ucp_sdk/models/schemas/common/types/location_distance.py deleted file mode 100644 index 1ac7f56..0000000 --- a/src/ucp_sdk/models/schemas/common/types/location_distance.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field - -from . import geo - - -class LocationDistance(BaseModel): - """ - An explicit-center inclusive-radius predicate. The Business compares the unrounded shortest WGS 84 ellipsoidal geodesic distance in RFC 7035 distance unit (meters) from `center` to the Location's authoritative `geo`; a value less than or equal to `max` matches. Implementations MAY use any algorithm that produces the WGS 84 inverse-geodesic result at sufficient precision such that the match outcome agrees with this unrounded comparison. No context, signals, IP, or `serves` fallback, radius clamping, tolerance, or operand substitution is permitted. - """ - - model_config = ConfigDict( - extra="allow", - ) - center: geo.Geo - """ - Explicit center of the radius. The Platform MUST supply it; the Business MUST NOT derive it from context, signals, an IP address, or `serves`. - """ - max: float = Field(..., ge=0.0) - """ - Inclusive maximum distance in RFC 7035 distance unit (meters). A Business unable to honor the supplied value MUST reject the request rather than clamp it or substitute another radius. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/location_filter.py b/src/ucp_sdk/models/schemas/common/types/location_filter.py deleted file mode 100644 index 81260f2..0000000 --- a/src/ucp_sdk/models/schemas/common/types/location_filter.py +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated - -from pydantic import ( - AwareDatetime, - BaseModel, - ConfigDict, - Field, - field_validator, -) -from typing_extensions import TypeAliasType - -from . import amenity_type - - -class Hours(BaseModel): - """ - Filter by operating hours, evaluated at the one supplied instant. - """ - - model_config = ConfigDict( - extra="allow", - ) - open_at: AwareDatetime = Field( - ..., pattern="(?:[Zz]|[+-](?:[01][0-9]|2[0-3]):[0-5][0-9])$" - ) - """ - The RFC 3339 instant at which matching Locations must be open, expressed with `Z` or a numeric offset. The Platform selects the instant that represents the Buyer's intent. The Business evaluates it exactly as supplied using each Location's authoritative `timezone`; the supplied offset does not identify that timezone. - """ - - -Item = TypeAliasType("Item", Annotated[str, Field(..., min_length=1)]) -""" -A non-empty, opaque Business-scoped item identifier. -""" - - -class LocationFilter(BaseModel): - """ - Filter criteria to narrow Location Search and Lookup results. All supplied filters combine with AND. - """ - - model_config = ConfigDict( - extra="allow", - ) - hours: Hours | None = None - """ - Filter by operating hours, evaluated at the one supplied instant. - """ - amenities: list[amenity_type.AmenityType] | None = None - """ - Filter by amenity identifier. A Location matches only when its `amenities` map contains every supplied identifier as an exact key; descriptions and namespace prefixes do not participate in matching. - """ - items: list[Item] | None = Field(None, min_length=1) - """ - Current item-availability filter. A candidate Location matches only when the Business can currently provide every referenced item at that Location; all references combine with AND. - """ - - @field_validator("items", mode="after") - def _enforce_unique_items_items(cls, value): # noqa: N805 - """JSON Schema uniqueItems: reject duplicate entries.""" - if value is None: - return value - seen = [] - for item in value: - if item in seen: - raise ValueError( - "Items must be unique (schema uniqueItems=true)" - ) - seen.append(item) - return value diff --git a/src/ucp_sdk/models/schemas/common/types/location_serves.py b/src/ucp_sdk/models/schemas/common/types/location_serves.py deleted file mode 100644 index 4b70a5a..0000000 --- a/src/ucp_sdk/models/schemas/common/types/location_serves.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field, model_validator - -from . import geo -from .locality import Locality - - -class Address(Locality): - """ - Coarse locality of the service target. - """ - - model_config = ConfigDict( - extra="allow", - ) - address_country: str = Field(..., min_length=1) - - -class Address1(Locality): - """ - Coarse locality of the service target. - """ - - model_config = ConfigDict( - extra="allow", - ) - address_region: str = Field(..., min_length=1) - - -class Address2(Locality): - """ - Coarse locality of the service target. - """ - - model_config = ConfigDict( - extra="allow", - ) - postal_code: str = Field(..., min_length=1) - - -class LocationServes(BaseModel): - """ - A one-entry map whose key names the authoritative service-target representation. The Platform MUST supply exactly one target form. A Business that cannot evaluate a well-formed target, or receives an extension form that was not negotiated, MUST reject the request rather than ignore it, fall back, or broaden results. This dictionary-like representation map cannot host an ambient `ucp` member. - """ - - model_config = ConfigDict( - extra="allow", - ) - point: geo.Geo | None = None - """ - WGS 84 coordinates of the service target. - """ - address: Address | Address1 | Address2 | None = None - """ - Coarse locality of the service target. - """ - - @model_validator(mode="after") - def _enforce_min_properties(self): - """JSON Schema minProperties: require at least 1 - provided property.""" - provided = self.model_fields_set | set(self.model_extra or {}) - if len(provided) < 1: - raise ValueError( - "At least 1 property must be provided (schema minProperties=1)" - ) - return self - - @model_validator(mode="after") - def _enforce_max_properties(self): - """JSON Schema maxProperties: allow at most 1 - provided property.""" - provided = self.model_fields_set | set(self.model_extra or {}) - if len(provided) > 1: - raise ValueError( - "At most 1 property may be provided (schema maxProperties=1)" - ) - return self diff --git a/src/ucp_sdk/models/schemas/common/types/location_serves_create_request.py b/src/ucp_sdk/models/schemas/common/types/location_serves_create_request.py deleted file mode 100644 index 89d4b13..0000000 --- a/src/ucp_sdk/models/schemas/common/types/location_serves_create_request.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field, model_validator - -from . import geo_create_request -from .locality_create_request import LocalityCreateRequest - - -class Address(LocalityCreateRequest): - """ - Coarse locality of the service target. - """ - - model_config = ConfigDict( - extra="allow", - ) - address_country: str = Field(..., min_length=1) - - -class Address4(LocalityCreateRequest): - """ - Coarse locality of the service target. - """ - - model_config = ConfigDict( - extra="allow", - ) - address_region: str = Field(..., min_length=1) - - -class Address5(LocalityCreateRequest): - """ - Coarse locality of the service target. - """ - - model_config = ConfigDict( - extra="allow", - ) - postal_code: str = Field(..., min_length=1) - - -class LocationServesCreateRequest(BaseModel): - """ - A one-entry map whose key names the authoritative service-target representation. The Platform MUST supply exactly one target form. A Business that cannot evaluate a well-formed target, or receives an extension form that was not negotiated, MUST reject the request rather than ignore it, fall back, or broaden results. This dictionary-like representation map cannot host an ambient `ucp` member. - """ - - model_config = ConfigDict( - extra="allow", - ) - point: geo_create_request.GeoCreateRequest | None = None - """ - WGS 84 coordinates of the service target. - """ - address: Address | Address4 | Address5 | None = None - """ - Coarse locality of the service target. - """ - - @model_validator(mode="after") - def _enforce_min_properties(self): - """JSON Schema minProperties: require at least 1 - provided property.""" - provided = self.model_fields_set | set(self.model_extra or {}) - if len(provided) < 1: - raise ValueError( - "At least 1 property must be provided (schema minProperties=1)" - ) - return self - - @model_validator(mode="after") - def _enforce_max_properties(self): - """JSON Schema maxProperties: allow at most 1 - provided property.""" - provided = self.model_fields_set | set(self.model_extra or {}) - if len(provided) > 1: - raise ValueError( - "At most 1 property may be provided (schema maxProperties=1)" - ) - return self diff --git a/src/ucp_sdk/models/schemas/common/types/location_serves_update_request.py b/src/ucp_sdk/models/schemas/common/types/location_serves_update_request.py deleted file mode 100644 index 94f4ab5..0000000 --- a/src/ucp_sdk/models/schemas/common/types/location_serves_update_request.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field, model_validator - -from . import geo_update_request -from .locality_update_request import LocalityUpdateRequest - - -class Address(LocalityUpdateRequest): - """ - Coarse locality of the service target. - """ - - model_config = ConfigDict( - extra="allow", - ) - address_country: str = Field(..., min_length=1) - - -class Address7(LocalityUpdateRequest): - """ - Coarse locality of the service target. - """ - - model_config = ConfigDict( - extra="allow", - ) - address_region: str = Field(..., min_length=1) - - -class Address8(LocalityUpdateRequest): - """ - Coarse locality of the service target. - """ - - model_config = ConfigDict( - extra="allow", - ) - postal_code: str = Field(..., min_length=1) - - -class LocationServesUpdateRequest(BaseModel): - """ - A one-entry map whose key names the authoritative service-target representation. The Platform MUST supply exactly one target form. A Business that cannot evaluate a well-formed target, or receives an extension form that was not negotiated, MUST reject the request rather than ignore it, fall back, or broaden results. This dictionary-like representation map cannot host an ambient `ucp` member. - """ - - model_config = ConfigDict( - extra="allow", - ) - point: geo_update_request.GeoUpdateRequest | None = None - """ - WGS 84 coordinates of the service target. - """ - address: Address | Address7 | Address8 | None = None - """ - Coarse locality of the service target. - """ - - @model_validator(mode="after") - def _enforce_min_properties(self): - """JSON Schema minProperties: require at least 1 - provided property.""" - provided = self.model_fields_set | set(self.model_extra or {}) - if len(provided) < 1: - raise ValueError( - "At least 1 property must be provided (schema minProperties=1)" - ) - return self - - @model_validator(mode="after") - def _enforce_max_properties(self): - """JSON Schema maxProperties: allow at most 1 - provided property.""" - provided = self.model_fields_set | set(self.model_extra or {}) - if len(provided) > 1: - raise ValueError( - "At most 1 property may be provided (schema maxProperties=1)" - ) - return self diff --git a/src/ucp_sdk/models/schemas/common/types/location_summary.py b/src/ucp_sdk/models/schemas/common/types/location_summary.py deleted file mode 100644 index 52bab23..0000000 --- a/src/ucp_sdk/models/schemas/common/types/location_summary.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - -from . import postal_address - - -class LocationSummary(BaseModel): - """ - A summary of a physical business location. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Stable, opaque, Business-scoped Location identifier. - """ - name: str - """ - Buyer-facing, Business-owned display name. - """ - address: postal_address.PostalAddress | None = None - """ - Physical address of the location. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/location_summary_create_request.py b/src/ucp_sdk/models/schemas/common/types/location_summary_create_request.py deleted file mode 100644 index dbb9a82..0000000 --- a/src/ucp_sdk/models/schemas/common/types/location_summary_create_request.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class LocationSummaryCreateRequest(BaseModel): - """ - A summary of a physical business location. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Stable, opaque, Business-scoped Location identifier. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/location_summary_update_request.py b/src/ucp_sdk/models/schemas/common/types/location_summary_update_request.py deleted file mode 100644 index 7460738..0000000 --- a/src/ucp_sdk/models/schemas/common/types/location_summary_update_request.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class LocationSummaryUpdateRequest(BaseModel): - """ - A summary of a physical business location. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Stable, opaque, Business-scoped Location identifier. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/location_update_request.py b/src/ucp_sdk/models/schemas/common/types/location_update_request.py deleted file mode 100644 index 8b5ecca..0000000 --- a/src/ucp_sdk/models/schemas/common/types/location_update_request.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field - -from .location_summary_update_request import LocationSummaryUpdateRequest - - -class LocationUpdateRequest(LocationSummaryUpdateRequest): - """ - The full, rich representation of a physical business location. Builds on the Location Summary schema with discovery-centric details such as geographic coordinates, operating hours, timezone, and amenities. - """ - - model_config = ConfigDict( - extra="allow", - ) - - -class Amenity(BaseModel): - """ - Buyer-facing presentation metadata for one amenity identifier. The containing map key, not this metadata, defines amenity identity and filter matching. - """ - - model_config = ConfigDict( - extra="allow", - ) - description: str = Field(..., min_length=1) - """ - Short, plain-text, buyer-facing label or phrase for the amenity, suitable for direct use in a compact list (e.g., 'Curbside pickup'). The Business SHOULD localize it for the request when possible. This content does not participate in amenity identity or filter matching. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/measure.py b/src/ucp_sdk/models/schemas/common/types/measure.py deleted file mode 100644 index f085f51..0000000 --- a/src/ucp_sdk/models/schemas/common/types/measure.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import ConfigDict, Field - -from .unit import Unit - - -class Measure(Unit): - """ - A measure composed of an integer value and a unit descriptor. Its value is the integer count of `10^-scale` units of `unit`. - """ - - model_config = ConfigDict( - extra="allow", - ) - value: int = Field(..., ge=-9007199254740991, le=9007199254740991) - """ - Integer count of `10^-scale` units of `unit`. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/measure_create_request.py b/src/ucp_sdk/models/schemas/common/types/measure_create_request.py deleted file mode 100644 index 006326d..0000000 --- a/src/ucp_sdk/models/schemas/common/types/measure_create_request.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import ConfigDict, Field - -from .unit_create_request import UnitCreateRequest - - -class MeasureCreateRequest(UnitCreateRequest): - """ - A measure composed of an integer value and a unit descriptor. Its value is the integer count of `10^-scale` units of `unit`. - """ - - model_config = ConfigDict( - extra="allow", - ) - value: int = Field(..., ge=-9007199254740991, le=9007199254740991) - """ - Integer count of `10^-scale` units of `unit`. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/measure_update_request.py b/src/ucp_sdk/models/schemas/common/types/measure_update_request.py deleted file mode 100644 index b2b08ea..0000000 --- a/src/ucp_sdk/models/schemas/common/types/measure_update_request.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import ConfigDict, Field - -from .unit_update_request import UnitUpdateRequest - - -class MeasureUpdateRequest(UnitUpdateRequest): - """ - A measure composed of an integer value and a unit descriptor. Its value is the integer count of `10^-scale` units of `unit`. - """ - - model_config = ConfigDict( - extra="allow", - ) - value: int = Field(..., ge=-9007199254740991, le=9007199254740991) - """ - Integer count of `10^-scale` units of `unit`. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/media.py b/src/ucp_sdk/models/schemas/common/types/media.py deleted file mode 100644 index b716792..0000000 --- a/src/ucp_sdk/models/schemas/common/types/media.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import AnyUrl, BaseModel, ConfigDict, Field - - -class Media(BaseModel): - """ - Media item (image, video, etc.). - """ - - model_config = ConfigDict( - extra="allow", - ) - type: str - """ - Media type. Well-known values: `image`, `video`, `model_3d`. - """ - url: AnyUrl - """ - URL to the media resource. - """ - alt_text: str | None = None - """ - Accessibility text describing the media. - """ - width: int | None = Field(None, ge=1) - """ - Width in pixels (for images/video). - """ - height: int | None = Field(None, ge=1) - """ - Height in pixels (for images/video). - """ diff --git a/src/ucp_sdk/models/schemas/common/types/message.py b/src/ucp_sdk/models/schemas/common/types/message.py index 65e20cc..7bcb41f 100644 --- a/src/ucp_sdk/models/schemas/common/types/message.py +++ b/src/ucp_sdk/models/schemas/common/types/message.py @@ -1,39 +1,7 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Message models.""" from __future__ import annotations -from typing import Annotated - -from pydantic import Field -from typing_extensions import TypeAliasType - -from . import message_error, message_info, message_warning +from ...models import Message, MessageError, MessageInfo, MessageWarning -Message = TypeAliasType( - "Message", - Annotated[ - message_error.MessageError - | message_warning.MessageWarning - | message_info.MessageInfo, - Field(..., title="Message"), - ], -) -""" -Container for error, warning, or info messages. -""" +__all__ = ["Message", "MessageError", "MessageInfo", "MessageWarning"] diff --git a/src/ucp_sdk/models/schemas/common/types/message_create_request.py b/src/ucp_sdk/models/schemas/common/types/message_create_request.py deleted file mode 100644 index 123e75d..0000000 --- a/src/ucp_sdk/models/schemas/common/types/message_create_request.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated - -from pydantic import Field -from typing_extensions import TypeAliasType - -from . import ( - message_error_create_request, - message_info_create_request, - message_warning_create_request, -) - -MessageCreateRequest = TypeAliasType( - "MessageCreateRequest", - Annotated[ - message_error_create_request.MessageErrorCreateRequest - | message_warning_create_request.MessageWarningCreateRequest - | message_info_create_request.MessageInfoCreateRequest, - Field(..., title="Message Create Request"), - ], -) -""" -Container for error, warning, or info messages. -""" diff --git a/src/ucp_sdk/models/schemas/common/types/message_error.py b/src/ucp_sdk/models/schemas/common/types/message_error.py deleted file mode 100644 index 9ac973c..0000000 --- a/src/ucp_sdk/models/schemas/common/types/message_error.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Literal - -from pydantic import BaseModel, ConfigDict - -from . import error_code - - -class MessageError(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - type: Literal["error"] - """ - Message type discriminator. - """ - code: error_code.ErrorCode - path: str | None = None - """ - RFC 9535 JSONPath to the component the message refers to (e.g., $.line_items[0]). - """ - content_type: Literal["plain", "markdown"] | None = "plain" - """ - Content format, default = plain. - """ - content: str - """ - Human-readable message. - """ - severity: Literal[ - "recoverable", - "requires_buyer_input", - "requires_buyer_review", - "unrecoverable", - ] - """ - Reflects the resource state and recommended action. 'recoverable': platform can resolve the condition in band, for example by modifying inputs or processing a related Action, and submit a new operation when needed. 'requires_buyer_input': merchant requires information their API doesn't support collecting programmatically (checkout incomplete). 'requires_buyer_review': buyer must authorize before order placement due to policy, regulatory, or entitlement rules. 'unrecoverable': no valid resource exists to act on, retry with new resource or inputs. Errors with 'requires_*' severity contribute to 'status: requires_escalation'. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/message_error_create_request.py b/src/ucp_sdk/models/schemas/common/types/message_error_create_request.py deleted file mode 100644 index cd13b78..0000000 --- a/src/ucp_sdk/models/schemas/common/types/message_error_create_request.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Literal - -from pydantic import BaseModel, ConfigDict - -from . import error_code_create_request - - -class MessageErrorCreateRequest(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - type: Literal["error"] - """ - Message type discriminator. - """ - code: error_code_create_request.ErrorCodeCreateRequest - path: str | None = None - """ - RFC 9535 JSONPath to the component the message refers to (e.g., $.line_items[0]). - """ - content_type: Literal["plain", "markdown"] | None = "plain" - """ - Content format, default = plain. - """ - content: str - """ - Human-readable message. - """ - severity: Literal[ - "recoverable", - "requires_buyer_input", - "requires_buyer_review", - "unrecoverable", - ] - """ - Reflects the resource state and recommended action. 'recoverable': platform can resolve the condition in band, for example by modifying inputs or processing a related Action, and submit a new operation when needed. 'requires_buyer_input': merchant requires information their API doesn't support collecting programmatically (checkout incomplete). 'requires_buyer_review': buyer must authorize before order placement due to policy, regulatory, or entitlement rules. 'unrecoverable': no valid resource exists to act on, retry with new resource or inputs. Errors with 'requires_*' severity contribute to 'status: requires_escalation'. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/message_error_update_request.py b/src/ucp_sdk/models/schemas/common/types/message_error_update_request.py deleted file mode 100644 index 94fcb77..0000000 --- a/src/ucp_sdk/models/schemas/common/types/message_error_update_request.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Literal - -from pydantic import BaseModel, ConfigDict - -from . import error_code_update_request - - -class MessageErrorUpdateRequest(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - type: Literal["error"] - """ - Message type discriminator. - """ - code: error_code_update_request.ErrorCodeUpdateRequest - path: str | None = None - """ - RFC 9535 JSONPath to the component the message refers to (e.g., $.line_items[0]). - """ - content_type: Literal["plain", "markdown"] | None = "plain" - """ - Content format, default = plain. - """ - content: str - """ - Human-readable message. - """ - severity: Literal[ - "recoverable", - "requires_buyer_input", - "requires_buyer_review", - "unrecoverable", - ] - """ - Reflects the resource state and recommended action. 'recoverable': platform can resolve the condition in band, for example by modifying inputs or processing a related Action, and submit a new operation when needed. 'requires_buyer_input': merchant requires information their API doesn't support collecting programmatically (checkout incomplete). 'requires_buyer_review': buyer must authorize before order placement due to policy, regulatory, or entitlement rules. 'unrecoverable': no valid resource exists to act on, retry with new resource or inputs. Errors with 'requires_*' severity contribute to 'status: requires_escalation'. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/message_info.py b/src/ucp_sdk/models/schemas/common/types/message_info.py deleted file mode 100644 index 2057e35..0000000 --- a/src/ucp_sdk/models/schemas/common/types/message_info.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Literal - -from pydantic import BaseModel, ConfigDict - -from . import info_code - - -class MessageInfo(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - type: Literal["info"] - """ - Message type discriminator. - """ - path: str | None = None - """ - RFC 9535 JSONPath to the component the message refers to (e.g., $.line_items[0]). - """ - code: info_code.InfoCode | None = None - content_type: Literal["plain", "markdown"] | None = "plain" - """ - Content format, default = plain. - """ - content: str - """ - Human-readable message. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/message_info_create_request.py b/src/ucp_sdk/models/schemas/common/types/message_info_create_request.py deleted file mode 100644 index 79b7738..0000000 --- a/src/ucp_sdk/models/schemas/common/types/message_info_create_request.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Literal - -from pydantic import BaseModel, ConfigDict - -from . import info_code_create_request - - -class MessageInfoCreateRequest(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - type: Literal["info"] - """ - Message type discriminator. - """ - path: str | None = None - """ - RFC 9535 JSONPath to the component the message refers to (e.g., $.line_items[0]). - """ - code: info_code_create_request.InfoCodeCreateRequest | None = None - content_type: Literal["plain", "markdown"] | None = "plain" - """ - Content format, default = plain. - """ - content: str - """ - Human-readable message. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/message_info_update_request.py b/src/ucp_sdk/models/schemas/common/types/message_info_update_request.py deleted file mode 100644 index 6a9f79c..0000000 --- a/src/ucp_sdk/models/schemas/common/types/message_info_update_request.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Literal - -from pydantic import BaseModel, ConfigDict - -from . import info_code_update_request - - -class MessageInfoUpdateRequest(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - type: Literal["info"] - """ - Message type discriminator. - """ - path: str | None = None - """ - RFC 9535 JSONPath to the component the message refers to (e.g., $.line_items[0]). - """ - code: info_code_update_request.InfoCodeUpdateRequest | None = None - content_type: Literal["plain", "markdown"] | None = "plain" - """ - Content format, default = plain. - """ - content: str - """ - Human-readable message. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/message_update_request.py b/src/ucp_sdk/models/schemas/common/types/message_update_request.py deleted file mode 100644 index 06e63e4..0000000 --- a/src/ucp_sdk/models/schemas/common/types/message_update_request.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated - -from pydantic import Field -from typing_extensions import TypeAliasType - -from . import ( - message_error_update_request, - message_info_update_request, - message_warning_update_request, -) - -MessageUpdateRequest = TypeAliasType( - "MessageUpdateRequest", - Annotated[ - message_error_update_request.MessageErrorUpdateRequest - | message_warning_update_request.MessageWarningUpdateRequest - | message_info_update_request.MessageInfoUpdateRequest, - Field(..., title="Message Update Request"), - ], -) -""" -Container for error, warning, or info messages. -""" diff --git a/src/ucp_sdk/models/schemas/common/types/message_warning.py b/src/ucp_sdk/models/schemas/common/types/message_warning.py deleted file mode 100644 index e816234..0000000 --- a/src/ucp_sdk/models/schemas/common/types/message_warning.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Literal - -from pydantic import AnyUrl, BaseModel, ConfigDict - -from . import warning_code - - -class MessageWarning(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - type: Literal["warning"] - """ - Message type discriminator. - """ - path: str | None = None - """ - RFC 9535 JSONPath to the component the message refers to (e.g., $.line_items[0]). - """ - code: warning_code.WarningCode - content: str - """ - Human-readable warning message that MUST be displayed. - """ - content_type: Literal["plain", "markdown"] | None = "plain" - """ - Content format, default = plain. - """ - presentation: str | None = "notice" - """ - Rendering contract for this warning. 'notice' (default): platform MUST display, MAY dismiss. 'disclosure': platform MUST display in proximity to the path-referenced component, MUST NOT hide or auto-dismiss. See specification for full contract. - """ - image_url: AnyUrl | None = None - """ - URL to a required visual element (e.g., warning symbol, energy class label). - """ - url: AnyUrl | None = None - """ - Reference URL for more information (e.g., regulatory site, registry entry, policy page). - """ diff --git a/src/ucp_sdk/models/schemas/common/types/message_warning_create_request.py b/src/ucp_sdk/models/schemas/common/types/message_warning_create_request.py deleted file mode 100644 index 712c72f..0000000 --- a/src/ucp_sdk/models/schemas/common/types/message_warning_create_request.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Literal - -from pydantic import AnyUrl, BaseModel, ConfigDict - -from . import warning_code_create_request - - -class MessageWarningCreateRequest(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - type: Literal["warning"] - """ - Message type discriminator. - """ - path: str | None = None - """ - RFC 9535 JSONPath to the component the message refers to (e.g., $.line_items[0]). - """ - code: warning_code_create_request.WarningCodeCreateRequest - content: str - """ - Human-readable warning message that MUST be displayed. - """ - content_type: Literal["plain", "markdown"] | None = "plain" - """ - Content format, default = plain. - """ - presentation: str | None = "notice" - """ - Rendering contract for this warning. 'notice' (default): platform MUST display, MAY dismiss. 'disclosure': platform MUST display in proximity to the path-referenced component, MUST NOT hide or auto-dismiss. See specification for full contract. - """ - image_url: AnyUrl | None = None - """ - URL to a required visual element (e.g., warning symbol, energy class label). - """ - url: AnyUrl | None = None - """ - Reference URL for more information (e.g., regulatory site, registry entry, policy page). - """ diff --git a/src/ucp_sdk/models/schemas/common/types/message_warning_update_request.py b/src/ucp_sdk/models/schemas/common/types/message_warning_update_request.py deleted file mode 100644 index 83944e6..0000000 --- a/src/ucp_sdk/models/schemas/common/types/message_warning_update_request.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Literal - -from pydantic import AnyUrl, BaseModel, ConfigDict - -from . import warning_code_update_request - - -class MessageWarningUpdateRequest(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - type: Literal["warning"] - """ - Message type discriminator. - """ - path: str | None = None - """ - RFC 9535 JSONPath to the component the message refers to (e.g., $.line_items[0]). - """ - code: warning_code_update_request.WarningCodeUpdateRequest - content: str - """ - Human-readable warning message that MUST be displayed. - """ - content_type: Literal["plain", "markdown"] | None = "plain" - """ - Content format, default = plain. - """ - presentation: str | None = "notice" - """ - Rendering contract for this warning. 'notice' (default): platform MUST display, MAY dismiss. 'disclosure': platform MUST display in proximity to the path-referenced component, MUST NOT hide or auto-dismiss. See specification for full contract. - """ - image_url: AnyUrl | None = None - """ - URL to a required visual element (e.g., warning symbol, energy class label). - """ - url: AnyUrl | None = None - """ - Reference URL for more information (e.g., regulatory site, registry entry, policy page). - """ diff --git a/src/ucp_sdk/models/schemas/common/types/network_token_credential.py b/src/ucp_sdk/models/schemas/common/types/network_token_credential.py deleted file mode 100644 index 522e07b..0000000 --- a/src/ucp_sdk/models/schemas/common/types/network_token_credential.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Literal - -from pydantic import ConfigDict, Field - -from .payment_credential import PaymentCredential - - -class NetworkTokenCredential(PaymentCredential): - """ - A card-network token credential verified with a transaction cryptogram. The `number` field carries the network token or wallet-provisioned token rather than the underlying FPAN. - """ - - model_config = ConfigDict( - extra="allow", - ) - type: Literal["network_token"] - """ - The credential type identifier for network token credentials. - """ - number: str = Field(..., examples=["5204240000004242"]) - """ - Network token or wallet-provisioned token replacing the underlying FPAN. - """ - expiry_month: int | None = None - """ - The month of the token's expiration date (1-12). - """ - expiry_year: int | None = None - """ - The year of the token's expiration date. - """ - name: str | None = Field(None, examples=["Jane Doe"]) - """ - Cardholder name. - """ - cryptogram: str = Field(..., examples=["gXc5UCLnM6ckD7pjM1TdPA=="]) - """ - Transaction cryptogram or dynamic CVC (dCVV), in the long or short form expected by the card network or processor. - """ - eci_value: str | None = Field(None, examples=["07"]) - """ - Electronic Commerce Indicator / Security Level Indicator associated with the transaction. - """ - token_requestor_id: str | None = Field(None, examples=["12345678901"]) - """ - Payment network token requestor identifier, when required by the processor or network-token program. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/pagination.py b/src/ucp_sdk/models/schemas/common/types/pagination.py deleted file mode 100644 index f33e089..0000000 --- a/src/ucp_sdk/models/schemas/common/types/pagination.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field, model_validator - - -class Pagination(BaseModel): - """ - Cursor-based pagination for list operations. - """ - - model_config = ConfigDict( - extra="allow", - ) - - -class Request(BaseModel): - """ - Pagination parameters for requests. - """ - - model_config = ConfigDict( - extra="allow", - ) - cursor: str | None = None - """ - Opaque cursor from previous response. - """ - limit: int | None = Field(None, ge=1) - """ - Requested page size, not a guaranteed result count. When omitted, the Business MUST apply a default page size. A default of 10 is RECOMMENDED, but the Business MAY choose another value. The Business MAY return fewer results than the requested or default page size, including when enforcing its maximum page size. A Platform MUST NOT assume that the response count equals either value. - """ - - -class Response(BaseModel): - """ - Pagination information in responses. - """ - - model_config = ConfigDict( - extra="allow", - ) - cursor: str | None = None - """ - Cursor to fetch the next page of results. MUST be present when has_next_page is true. - """ - has_next_page: bool - """ - Whether more results are available. - """ - total_count: int | None = Field(None, ge=0) - """ - Total number of matching items, if available. - """ - - @model_validator(mode="after") - def _enforce_conditional_required(self): - """JSON Schema if/then: enforce conditionally required fields.""" - rules = [ - { - "discriminator": "has_next_page", - "values": [True], - "required": ["cursor"], - } - ] - for rule in rules: - if getattr(self, rule["discriminator"], None) not in rule["values"]: - continue - for field in rule["required"]: - if field not in self.model_fields_set: - raise ValueError( - f"Field {field!r} is required by a schema condition" - ) - return self diff --git a/src/ucp_sdk/models/schemas/common/types/pan_credential.py b/src/ucp_sdk/models/schemas/common/types/pan_credential.py deleted file mode 100644 index f4eb86e..0000000 --- a/src/ucp_sdk/models/schemas/common/types/pan_credential.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Literal - -from pydantic import ConfigDict, Field - -from .payment_credential import PaymentCredential - - -class PanCredential(PaymentCredential): - """ - A card credential carrying a funding primary account number (FPAN). Credential selection follows the shape of the value on the wire rather than its provenance: a network token surfaced in PAN form and verified with a `cvc` - as with credentials where a dynamic verification code proxies the cryptogram - is carried here, while a token verified with a discrete `cryptogram` uses Network Token Credential. This credential type MUST NOT be used for checkout, only with payment handlers that tokenize or encrypt credentials. CRITICAL: Both parties handling a PAN credential (sender and receiver) MUST be PCI DSS compliant. Transmission MUST use HTTPS/TLS with strong cipher suites. - """ - - model_config = ConfigDict( - extra="allow", - ) - type: Literal["pan"] - """ - The credential type identifier for PAN credentials. - """ - number: str = Field(..., examples=["4242424242424242"]) - """ - Funding primary account number (FPAN). - """ - expiry_month: int | None = None - """ - The month of the card's expiration date (1-12). - """ - expiry_year: int | None = None - """ - The year of the card's expiration date. - """ - name: str | None = Field(None, examples=["Jane Doe"]) - """ - Cardholder name. - """ - cvc: str | None = Field(None, examples=["223"], max_length=4) - """ - Card verification code. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/payment.py b/src/ucp_sdk/models/schemas/common/types/payment.py index 1f6106a..e7bf5f0 100644 --- a/src/ucp_sdk/models/schemas/common/types/payment.py +++ b/src/ucp_sdk/models/schemas/common/types/payment.py @@ -1,39 +1,7 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Payment models.""" from __future__ import annotations -from pydantic import BaseModel, ConfigDict - -from . import payment_instrument - - -class Payment(BaseModel): - """ - Payment configuration containing handlers. - """ +from ...models import Payment, PaymentCredential - model_config = ConfigDict( - extra="allow", - ) - instruments: list[payment_instrument.SelectedPaymentInstrument] | None = ( - None - ) - """ - The payment instruments available for this payment. Each instrument is associated with a specific handler via the handler_id field. Handlers can extend the base payment_instrument schema to add handler-specific fields. - """ +__all__ = ["Payment", "PaymentCredential"] diff --git a/src/ucp_sdk/models/schemas/common/types/payment_complete_request.py b/src/ucp_sdk/models/schemas/common/types/payment_complete_request.py deleted file mode 100644 index 8955569..0000000 --- a/src/ucp_sdk/models/schemas/common/types/payment_complete_request.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - -from . import payment_instrument_complete_request - - -class PaymentCompleteRequest(BaseModel): - """ - Payment configuration containing handlers. - """ - - model_config = ConfigDict( - extra="allow", - ) - instruments: ( - list[payment_instrument_complete_request.SelectedPaymentInstrument] - | None - ) = None - """ - The payment instruments available for this payment. Each instrument is associated with a specific handler via the handler_id field. Handlers can extend the base payment_instrument schema to add handler-specific fields. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/payment_create_request.py b/src/ucp_sdk/models/schemas/common/types/payment_create_request.py deleted file mode 100644 index 691a23c..0000000 --- a/src/ucp_sdk/models/schemas/common/types/payment_create_request.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - -from . import payment_instrument_create_request - - -class PaymentCreateRequest(BaseModel): - """ - Payment configuration containing handlers. - """ - - model_config = ConfigDict( - extra="allow", - ) - instruments: ( - list[payment_instrument_create_request.SelectedPaymentInstrument] | None - ) = None - """ - The payment instruments available for this payment. Each instrument is associated with a specific handler via the handler_id field. Handlers can extend the base payment_instrument schema to add handler-specific fields. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/payment_credential.py b/src/ucp_sdk/models/schemas/common/types/payment_credential.py deleted file mode 100644 index 4a4e09d..0000000 --- a/src/ucp_sdk/models/schemas/common/types/payment_credential.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class PaymentCredential(BaseModel): - """ - The base definition for any payment credential. Handlers define specific credential types. - """ - - model_config = ConfigDict( - extra="allow", - ) - type: str - """ - The credential type discriminator. Specific schemas will constrain this to a constant value. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/payment_credential_complete_request.py b/src/ucp_sdk/models/schemas/common/types/payment_credential_complete_request.py deleted file mode 100644 index 0fe4d9c..0000000 --- a/src/ucp_sdk/models/schemas/common/types/payment_credential_complete_request.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class PaymentCredentialCompleteRequest(BaseModel): - """ - The base definition for any payment credential. Handlers define specific credential types. - """ - - model_config = ConfigDict( - extra="allow", - ) - type: str - """ - The credential type discriminator. Specific schemas will constrain this to a constant value. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/payment_credential_create_request.py b/src/ucp_sdk/models/schemas/common/types/payment_credential_create_request.py deleted file mode 100644 index 0c79bf1..0000000 --- a/src/ucp_sdk/models/schemas/common/types/payment_credential_create_request.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class PaymentCredentialCreateRequest(BaseModel): - """ - The base definition for any payment credential. Handlers define specific credential types. - """ - - model_config = ConfigDict( - extra="allow", - ) - type: str - """ - The credential type discriminator. Specific schemas will constrain this to a constant value. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/payment_credential_update_request.py b/src/ucp_sdk/models/schemas/common/types/payment_credential_update_request.py deleted file mode 100644 index 9a6eb10..0000000 --- a/src/ucp_sdk/models/schemas/common/types/payment_credential_update_request.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class PaymentCredentialUpdateRequest(BaseModel): - """ - The base definition for any payment credential. Handlers define specific credential types. - """ - - model_config = ConfigDict( - extra="allow", - ) - type: str - """ - The credential type discriminator. Specific schemas will constrain this to a constant value. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/payment_identity.py b/src/ucp_sdk/models/schemas/common/types/payment_identity.py deleted file mode 100644 index 14b1881..0000000 --- a/src/ucp_sdk/models/schemas/common/types/payment_identity.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class PaymentIdentity(BaseModel): - """ - Identity of a participant for token binding. The access_token uniquely identifies the participant whom tokens should be issued to. - """ - - model_config = ConfigDict( - extra="allow", - ) - access_token: str - """ - Unique identifier for this participant, obtained during onboarding with the tokenizer. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/payment_instrument.py b/src/ucp_sdk/models/schemas/common/types/payment_instrument.py index 3570af2..2d5bcc1 100644 --- a/src/ucp_sdk/models/schemas/common/types/payment_instrument.py +++ b/src/ucp_sdk/models/schemas/common/types/payment_instrument.py @@ -1,70 +1,15 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Payment instrument models.""" from __future__ import annotations -from typing import Any - -from pydantic import BaseModel, ConfigDict - -from . import payment_credential, postal_address - - -class PaymentInstrument(BaseModel): - """ - The base definition for any payment instrument. It links the instrument to a specific payment handler. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - A unique identifier for this instrument instance. Typically assigned by the platform for instruments it collects. For a business-owned saved instrument returned on an identity-linked response, this identifier is assigned by the business; the platform MUST treat it as an opaque, business-scoped reference, and the business resolves it server-side when the buyer selects it. - """ - handler_id: str - """ - The unique identifier for the handler instance that produced this instrument. This corresponds to the 'id' field in the Payment Handler definition. - """ - type: str - """ - The broad category of the instrument (e.g., 'card', 'tokenized_card'). Specific schemas will constrain this to a constant value. - """ - billing_address: postal_address.PostalAddress | None = None - """ - The billing address associated with this payment method. - """ - credential: payment_credential.PaymentCredential | None = None - display: dict[str, Any] | None = None - """ - Display information for this payment instrument. Each payment instrument schema defines its specific display properties, as outlined by the payment handler. - """ - - -class SelectedPaymentInstrument(PaymentInstrument): - """ - A payment instrument with selection state. - """ - - model_config = ConfigDict( - extra="allow", - ) - selected: bool | None = None - """ - Whether this instrument is selected by the user. - """ +from ...models import ( + AvailablePaymentInstrument, + PaymentInstrument, + SelectedPaymentInstrument, +) + +__all__ = [ + "PaymentInstrument", + "AvailablePaymentInstrument", + "SelectedPaymentInstrument", +] diff --git a/src/ucp_sdk/models/schemas/common/types/payment_instrument_complete_request.py b/src/ucp_sdk/models/schemas/common/types/payment_instrument_complete_request.py deleted file mode 100644 index 8109560..0000000 --- a/src/ucp_sdk/models/schemas/common/types/payment_instrument_complete_request.py +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Any - -from pydantic import BaseModel, ConfigDict - -from . import ( - payment_credential_complete_request, - postal_address_complete_request, -) - - -class PaymentInstrumentCompleteRequest(BaseModel): - """ - The base definition for any payment instrument. It links the instrument to a specific payment handler. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - A unique identifier for this instrument instance. Typically assigned by the platform for instruments it collects. For a business-owned saved instrument returned on an identity-linked response, this identifier is assigned by the business; the platform MUST treat it as an opaque, business-scoped reference, and the business resolves it server-side when the buyer selects it. - """ - handler_id: str - """ - The unique identifier for the handler instance that produced this instrument. This corresponds to the 'id' field in the Payment Handler definition. - """ - type: str - """ - The broad category of the instrument (e.g., 'card', 'tokenized_card'). Specific schemas will constrain this to a constant value. - """ - billing_address: ( - postal_address_complete_request.PostalAddressCompleteRequest | None - ) = None - """ - The billing address associated with this payment method. - """ - credential: ( - payment_credential_complete_request.PaymentCredentialCompleteRequest - | None - ) = None - display: dict[str, Any] | None = None - """ - Display information for this payment instrument. Each payment instrument schema defines its specific display properties, as outlined by the payment handler. - """ - - -class SelectedPaymentInstrument(PaymentInstrumentCompleteRequest): - """ - A payment instrument with selection state. - """ - - model_config = ConfigDict( - extra="allow", - ) - selected: bool | None = None - """ - Whether this instrument is selected by the user. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/payment_instrument_create_request.py b/src/ucp_sdk/models/schemas/common/types/payment_instrument_create_request.py deleted file mode 100644 index 8d000b5..0000000 --- a/src/ucp_sdk/models/schemas/common/types/payment_instrument_create_request.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Any - -from pydantic import BaseModel, ConfigDict - -from . import payment_credential_create_request, postal_address_create_request - - -class PaymentInstrumentCreateRequest(BaseModel): - """ - The base definition for any payment instrument. It links the instrument to a specific payment handler. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - A unique identifier for this instrument instance. Typically assigned by the platform for instruments it collects. For a business-owned saved instrument returned on an identity-linked response, this identifier is assigned by the business; the platform MUST treat it as an opaque, business-scoped reference, and the business resolves it server-side when the buyer selects it. - """ - handler_id: str - """ - The unique identifier for the handler instance that produced this instrument. This corresponds to the 'id' field in the Payment Handler definition. - """ - type: str - """ - The broad category of the instrument (e.g., 'card', 'tokenized_card'). Specific schemas will constrain this to a constant value. - """ - billing_address: ( - postal_address_create_request.PostalAddressCreateRequest | None - ) = None - """ - The billing address associated with this payment method. - """ - credential: ( - payment_credential_create_request.PaymentCredentialCreateRequest | None - ) = None - display: dict[str, Any] | None = None - """ - Display information for this payment instrument. Each payment instrument schema defines its specific display properties, as outlined by the payment handler. - """ - - -class SelectedPaymentInstrument(PaymentInstrumentCreateRequest): - """ - A payment instrument with selection state. - """ - - model_config = ConfigDict( - extra="allow", - ) - selected: bool | None = None - """ - Whether this instrument is selected by the user. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/payment_instrument_update_request.py b/src/ucp_sdk/models/schemas/common/types/payment_instrument_update_request.py deleted file mode 100644 index 825dbd0..0000000 --- a/src/ucp_sdk/models/schemas/common/types/payment_instrument_update_request.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Any - -from pydantic import BaseModel, ConfigDict - -from . import payment_credential_update_request, postal_address_update_request - - -class PaymentInstrumentUpdateRequest(BaseModel): - """ - The base definition for any payment instrument. It links the instrument to a specific payment handler. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - A unique identifier for this instrument instance. Typically assigned by the platform for instruments it collects. For a business-owned saved instrument returned on an identity-linked response, this identifier is assigned by the business; the platform MUST treat it as an opaque, business-scoped reference, and the business resolves it server-side when the buyer selects it. - """ - handler_id: str - """ - The unique identifier for the handler instance that produced this instrument. This corresponds to the 'id' field in the Payment Handler definition. - """ - type: str - """ - The broad category of the instrument (e.g., 'card', 'tokenized_card'). Specific schemas will constrain this to a constant value. - """ - billing_address: ( - postal_address_update_request.PostalAddressUpdateRequest | None - ) = None - """ - The billing address associated with this payment method. - """ - credential: ( - payment_credential_update_request.PaymentCredentialUpdateRequest | None - ) = None - display: dict[str, Any] | None = None - """ - Display information for this payment instrument. Each payment instrument schema defines its specific display properties, as outlined by the payment handler. - """ - - -class SelectedPaymentInstrument(PaymentInstrumentUpdateRequest): - """ - A payment instrument with selection state. - """ - - model_config = ConfigDict( - extra="allow", - ) - selected: bool | None = None - """ - Whether this instrument is selected by the user. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/payment_schedule.py b/src/ucp_sdk/models/schemas/common/types/payment_schedule.py deleted file mode 100644 index 865aade..0000000 --- a/src/ucp_sdk/models/schemas/common/types/payment_schedule.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import AwareDatetime, BaseModel, ConfigDict - -from . import amount as amount_1 -from . import description as description_1 - - -class PaymentSchedule(BaseModel): - """ - A single payment that settles part or all of the checkout under a payment term. Timing is stated in buyer-facing text; `type` and `due_at` are supplementary machine-readable signals derived from it. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Identifier for this payment schedule, unique within its payment term. Businesses SHOULD keep it stable across responses while the schedule remains the same payment. - """ - type: str - """ - Timing class, drawn from an open vocabulary. `immediate` is the only value with defined meaning: the payment is due when the checkout is completed. Any other value means the payment is not due at completion, and `description` states when it is due. Whether a due payment is authorized, captured, or settled at that moment is payment-handler behavior and outside this extension. Businesses MAY use additional values (e.g. `deferred`, `on_shipment`); Platforms MUST treat unrecognized values as not due at completion. - """ - description: description_1.Description - """ - Complete buyer-facing statement of when and how this payment is due. Businesses MUST make this field sufficient on its own: a Platform that recognizes no `type` value and reads no other field MUST be able to present this schedule correctly. Platforms MAY use `type` and `due_at` for enhanced presentation, but MUST NOT present derived timing that contradicts this field. - """ - due_at: AwareDatetime | None = None - """ - Absolute RFC 3339 date-time when this payment is due, when the Business can determine one at checkout. Supplementary to `description`, never a replacement for it. Omitted when the due date depends on a future event (e.g. 'due on delivery'); the timing is then stated in `description` alone. - """ - amount: amount_1.Amount - """ - The amount charged when this payment is taken, inclusive of tax and every other charge, in the Checkout currency's minor units (ISO 4217). A schedule states an amount rather than a totals breakdown: the purchase is priced once at the Checkout, and a schedule moves part or all of that price. Where the selected term changes what the purchase costs, that difference appears in `checkout.totals`, not here. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/payment_term.py b/src/ucp_sdk/models/schemas/common/types/payment_term.py deleted file mode 100644 index e2a6ee4..0000000 --- a/src/ucp_sdk/models/schemas/common/types/payment_term.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field - -from . import description as description_1 -from . import payment_schedule - - -class PaymentTerm(BaseModel): - """ - A way of paying for the checkout: one or more payment schedules that together cover its total. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Unique identifier for this payment term within the checkout. Referenced by `payment.selected_term_id`. - """ - title: str - """ - Short label that distinguishes this term from its siblings (e.g. 'Pay now', 'Pay in 4', 'Deposit + balance at check-in'). - """ - description: description_1.Description | None = None - """ - Supplementary context for the title (e.g. 'Save 5% by paying today'). Directly renderable; MUST NOT repeat the title. - """ - schedules: list[payment_schedule.PaymentSchedule] = Field(..., min_length=1) - """ - Payment schedules that settle this checkout under this term, in the order they come due. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/payment_update_request.py b/src/ucp_sdk/models/schemas/common/types/payment_update_request.py deleted file mode 100644 index e6c3658..0000000 --- a/src/ucp_sdk/models/schemas/common/types/payment_update_request.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - -from . import payment_instrument_update_request - - -class PaymentUpdateRequest(BaseModel): - """ - Payment configuration containing handlers. - """ - - model_config = ConfigDict( - extra="allow", - ) - instruments: ( - list[payment_instrument_update_request.SelectedPaymentInstrument] | None - ) = None - """ - The payment instruments available for this payment. Each instrument is associated with a specific handler via the handler_id field. Handlers can extend the base payment_instrument schema to add handler-specific fields. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/policy.py b/src/ucp_sdk/models/schemas/common/types/policy.py index 9db2ca7..8ac3286 100644 --- a/src/ucp_sdk/models/schemas/common/types/policy.py +++ b/src/ucp_sdk/models/schemas/common/types/policy.py @@ -1,50 +1,7 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Policy models.""" from __future__ import annotations -from pydantic import AnyUrl, BaseModel, ConfigDict - -from . import description as description_1 -from . import reverse_domain_name - - -class Policy(BaseModel): - """ - A durable business rule about the items in a response — return/refund terms, warranty, and the like — at the time of purchase. Every policy carries a `type` (an open reverse-DNS vocabulary) and a `description` so a platform can present it without understanding its type-specific fields; type-specific fields (gated by `type`) add structured context for platforms that model that type. Policies are reference data; the obligation to display a term to the buyer is carried by a `messages[]` warning whose `code` equals the policy `type` — see the Policies section of the specification. - """ +from ...models import Policy - model_config = ConfigDict( - extra="allow", - ) - type: reverse_domain_name.ReverseDomainName - """ - Policy type discriminator. Open reverse-DNS vocabulary. Well-known values: `dev.ucp.shopping.policy.return` (return terms), `dev.ucp.shopping.policy.warranty` (warranty terms). Businesses MAY define custom types in their own domain (e.g., `com.example.policy.price_match`). Platforms MUST tolerate unknown values. - """ - description: description_1.Description - """ - Human-readable policy summary in one or more formats (plain, markdown, html). Required on every policy so a platform can present it without understanding any type-specific fields. This is not the buyer-facing disclosure — display is compelled by a `messages[]` warning (see the Policies section). - """ - applies_to: list[str] | None = None - """ - RFC 9535 JSONPath expressions identifying the nodes this policy applies to, relative to the embedding response root (e.g., `$.line_items[0]` in cart/checkout, `$.products[2]` in catalog). Each target covers the node it names and everything nested under it, so a target on a product also covers its variants. A singular query (RFC 9535 Section 2.3.5.1; name and index selectors only) names a single node; filters, wildcards, and slices match a set. When omitted, the policy applies to the entire response. When policies of the same `type` contest a node, the narrowest target wins and overrides the rest. See the Policies section for how specificity resolves. - """ - url: AnyUrl | None = None - """ - Optional link to the full policy document. - """ +__all__ = ["Policy"] diff --git a/src/ucp_sdk/models/schemas/common/types/postal_address.py b/src/ucp_sdk/models/schemas/common/types/postal_address.py index 7c57364..13e5575 100644 --- a/src/ucp_sdk/models/schemas/common/types/postal_address.py +++ b/src/ucp_sdk/models/schemas/common/types/postal_address.py @@ -1,63 +1,7 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Postal address models.""" from __future__ import annotations -from pydantic import BaseModel, ConfigDict - +from ...models import PostalAddress -class PostalAddress(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - extended_address: str | None = None - """ - An address extension such as an apartment number, C/O or alternative name. - """ - street_address: str | None = None - """ - The street address. - """ - address_locality: str | None = None - """ - The locality in which the street address is, and which is in the region. For example, Mountain View. - """ - address_region: str | None = None - """ - The region in which the locality is, and which is in the country. Required for applicable countries (i.e. state in US, province in CA). For example, California or another appropriate first-level Administrative division. - """ - address_country: str | None = None - """ - The country. Recommended to be in 2-letter ISO 3166-1 alpha-2 format, for example "US". For backward compatibility, a 3-letter ISO 3166-1 alpha-3 country code such as "SGP" or a full country name such as "Singapore" can also be used. - """ - postal_code: str | None = None - """ - The postal code. For example, 94043. - """ - first_name: str | None = None - """ - Optional. First name of the contact associated with the address. - """ - last_name: str | None = None - """ - Optional. Last name of the contact associated with the address. - """ - phone_number: str | None = None - """ - Optional. Phone number of the contact associated with the address. - """ +__all__ = ["PostalAddress"] diff --git a/src/ucp_sdk/models/schemas/common/types/postal_address_complete_request.py b/src/ucp_sdk/models/schemas/common/types/postal_address_complete_request.py deleted file mode 100644 index c282a9d..0000000 --- a/src/ucp_sdk/models/schemas/common/types/postal_address_complete_request.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class PostalAddressCompleteRequest(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - extended_address: str | None = None - """ - An address extension such as an apartment number, C/O or alternative name. - """ - street_address: str | None = None - """ - The street address. - """ - address_locality: str | None = None - """ - The locality in which the street address is, and which is in the region. For example, Mountain View. - """ - address_region: str | None = None - """ - The region in which the locality is, and which is in the country. Required for applicable countries (i.e. state in US, province in CA). For example, California or another appropriate first-level Administrative division. - """ - address_country: str | None = None - """ - The country. Recommended to be in 2-letter ISO 3166-1 alpha-2 format, for example "US". For backward compatibility, a 3-letter ISO 3166-1 alpha-3 country code such as "SGP" or a full country name such as "Singapore" can also be used. - """ - postal_code: str | None = None - """ - The postal code. For example, 94043. - """ - first_name: str | None = None - """ - Optional. First name of the contact associated with the address. - """ - last_name: str | None = None - """ - Optional. Last name of the contact associated with the address. - """ - phone_number: str | None = None - """ - Optional. Phone number of the contact associated with the address. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/postal_address_create_request.py b/src/ucp_sdk/models/schemas/common/types/postal_address_create_request.py deleted file mode 100644 index cdf804f..0000000 --- a/src/ucp_sdk/models/schemas/common/types/postal_address_create_request.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class PostalAddressCreateRequest(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - extended_address: str | None = None - """ - An address extension such as an apartment number, C/O or alternative name. - """ - street_address: str | None = None - """ - The street address. - """ - address_locality: str | None = None - """ - The locality in which the street address is, and which is in the region. For example, Mountain View. - """ - address_region: str | None = None - """ - The region in which the locality is, and which is in the country. Required for applicable countries (i.e. state in US, province in CA). For example, California or another appropriate first-level Administrative division. - """ - address_country: str | None = None - """ - The country. Recommended to be in 2-letter ISO 3166-1 alpha-2 format, for example "US". For backward compatibility, a 3-letter ISO 3166-1 alpha-3 country code such as "SGP" or a full country name such as "Singapore" can also be used. - """ - postal_code: str | None = None - """ - The postal code. For example, 94043. - """ - first_name: str | None = None - """ - Optional. First name of the contact associated with the address. - """ - last_name: str | None = None - """ - Optional. Last name of the contact associated with the address. - """ - phone_number: str | None = None - """ - Optional. Phone number of the contact associated with the address. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/postal_address_update_request.py b/src/ucp_sdk/models/schemas/common/types/postal_address_update_request.py deleted file mode 100644 index 35b3f6f..0000000 --- a/src/ucp_sdk/models/schemas/common/types/postal_address_update_request.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class PostalAddressUpdateRequest(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - extended_address: str | None = None - """ - An address extension such as an apartment number, C/O or alternative name. - """ - street_address: str | None = None - """ - The street address. - """ - address_locality: str | None = None - """ - The locality in which the street address is, and which is in the region. For example, Mountain View. - """ - address_region: str | None = None - """ - The region in which the locality is, and which is in the country. Required for applicable countries (i.e. state in US, province in CA). For example, California or another appropriate first-level Administrative division. - """ - address_country: str | None = None - """ - The country. Recommended to be in 2-letter ISO 3166-1 alpha-2 format, for example "US". For backward compatibility, a 3-letter ISO 3166-1 alpha-3 country code such as "SGP" or a full country name such as "Singapore" can also be used. - """ - postal_code: str | None = None - """ - The postal code. For example, 94043. - """ - first_name: str | None = None - """ - Optional. First name of the contact associated with the address. - """ - last_name: str | None = None - """ - Optional. Last name of the contact associated with the address. - """ - phone_number: str | None = None - """ - Optional. Phone number of the contact associated with the address. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/price.py b/src/ucp_sdk/models/schemas/common/types/price.py deleted file mode 100644 index aff26a1..0000000 --- a/src/ucp_sdk/models/schemas/common/types/price.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field - -from . import amount as amount_1 - - -class Price(BaseModel): - """ - Price with explicit currency. - """ - - model_config = ConfigDict( - extra="allow", - ) - amount: amount_1.Amount - """ - Amount in ISO 4217 minor units. Use 0 for free items. - """ - currency: str = Field(..., pattern="^[A-Z]{3}$") - """ - ISO 4217 currency code (e.g., 'USD', 'EUR', 'GBP'). - """ diff --git a/src/ucp_sdk/models/schemas/common/types/price_filter.py b/src/ucp_sdk/models/schemas/common/types/price_filter.py deleted file mode 100644 index 7772e95..0000000 --- a/src/ucp_sdk/models/schemas/common/types/price_filter.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - -from . import amount - - -class PriceFilter(BaseModel): - """ - Price range filter denominated in context.currency. When context.currency matches the presentment currency, businesses apply the filter directly. When it differs, businesses SHOULD convert filter values to the presentment currency before applying; if conversion is not supported, businesses MAY ignore the filter and SHOULD indicate this via a message. When context.currency is absent, filter denomination is ambiguous and businesses MAY ignore it. - """ - - model_config = ConfigDict( - extra="allow", - ) - min: amount.Amount | None = None - """ - Minimum price in ISO 4217 minor units. - """ - max: amount.Amount | None = None - """ - Maximum price in ISO 4217 minor units. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/price_range.py b/src/ucp_sdk/models/schemas/common/types/price_range.py deleted file mode 100644 index 1c8e47b..0000000 --- a/src/ucp_sdk/models/schemas/common/types/price_range.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - -from . import price - - -class PriceRange(BaseModel): - """ - A price range representing minimum and maximum values (e.g., a common example in retail shopping is when prices vary across product variants). - """ - - model_config = ConfigDict( - extra="allow", - ) - min: price.Price - """ - Minimum price in the range. - """ - max: price.Price - """ - Maximum price in the range. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/quantity_unit.py b/src/ucp_sdk/models/schemas/common/types/quantity_unit.py deleted file mode 100644 index 44d14dd..0000000 --- a/src/ucp_sdk/models/schemas/common/types/quantity_unit.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import ConfigDict, Field - -from .unit import Unit - - -class QuantityUnit(Unit): - """ - Sale-basis descriptor for quantities: the shared unit descriptor plus the Business's ordering policy. Its unit-descriptor machine identity remains (`unit`, effective `scale`); `display_text` and `increment` are excluded from identity and mismatch comparison. - """ - - model_config = ConfigDict( - extra="allow", - ) - increment: int | None = Field(1, ge=1) - """ - Ordering granularity, denominated in steps: the Business sells this item in integer multiples of `increment` steps. Its effective value is the provided value or 1. Advisory merchandising policy, not a representational bound: Platform-authored quantities SHOULD be integer multiples of the effective increment; the Business MAY accept, revise, or reject an off-increment request with a recoverable business outcome and MUST NOT silently reinterpret it. Business-authored quantities (checkout revisions, fulfillment events, adjustments) are bounded only by `scale`. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/quantity_unit_create_request.py b/src/ucp_sdk/models/schemas/common/types/quantity_unit_create_request.py deleted file mode 100644 index 7a38b38..0000000 --- a/src/ucp_sdk/models/schemas/common/types/quantity_unit_create_request.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import ConfigDict, Field - -from .unit_create_request import UnitCreateRequest - - -class QuantityUnitCreateRequest(UnitCreateRequest): - """ - Sale-basis descriptor for quantities: the shared unit descriptor plus the Business's ordering policy. Its unit-descriptor machine identity remains (`unit`, effective `scale`); `display_text` and `increment` are excluded from identity and mismatch comparison. - """ - - model_config = ConfigDict( - extra="allow", - ) - increment: int | None = Field(1, ge=1) - """ - Ordering granularity, denominated in steps: the Business sells this item in integer multiples of `increment` steps. Its effective value is the provided value or 1. Advisory merchandising policy, not a representational bound: Platform-authored quantities SHOULD be integer multiples of the effective increment; the Business MAY accept, revise, or reject an off-increment request with a recoverable business outcome and MUST NOT silently reinterpret it. Business-authored quantities (checkout revisions, fulfillment events, adjustments) are bounded only by `scale`. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/quantity_unit_update_request.py b/src/ucp_sdk/models/schemas/common/types/quantity_unit_update_request.py deleted file mode 100644 index cadcad3..0000000 --- a/src/ucp_sdk/models/schemas/common/types/quantity_unit_update_request.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import ConfigDict, Field - -from .unit_update_request import UnitUpdateRequest - - -class QuantityUnitUpdateRequest(UnitUpdateRequest): - """ - Sale-basis descriptor for quantities: the shared unit descriptor plus the Business's ordering policy. Its unit-descriptor machine identity remains (`unit`, effective `scale`); `display_text` and `increment` are excluded from identity and mismatch comparison. - """ - - model_config = ConfigDict( - extra="allow", - ) - increment: int | None = Field(1, ge=1) - """ - Ordering granularity, denominated in steps: the Business sells this item in integer multiples of `increment` steps. Its effective value is the provided value or 1. Advisory merchandising policy, not a representational bound: Platform-authored quantities SHOULD be integer multiples of the effective increment; the Business MAY accept, revise, or reject an off-increment request with a recoverable business outcome and MUST NOT silently reinterpret it. Business-authored quantities (checkout revisions, fulfillment events, adjustments) are bounded only by `scale`. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/request_constraints.py b/src/ucp_sdk/models/schemas/common/types/request_constraints.py deleted file mode 100644 index 58b3294..0000000 --- a/src/ucp_sdk/models/schemas/common/types/request_constraints.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field, field_validator - -from . import constraint_expression - - -class RequestConstraints(BaseModel): - """ - Binds the shared Constraint Expression grammar to data in the next UCP request to the same resource. - """ - - model_config = ConfigDict( - extra="forbid", - ) - path: str | None = None - """ - A complete RFC 9535 JSONPath query evaluated against the next logical UCP request to the same resource. - """ - required: list[str] | None = Field(None, min_length=1) - """ - Property names required by the constrained object. Must be non-empty: an empty array applies no constraint. - """ - properties: ( - dict[ - str, - constraint_expression.ConstraintExpression - | constraint_expression.ValueConstraint, - ] - | None - ) = Field(None, min_length=1) - """ - Constraints keyed by property name. Must be non-empty: an empty object applies no constraint. - """ - anyOf: list[constraint_expression.ConstraintExpression] | None = Field( - None, min_length=1 - ) - """ - Alternative Object Constraints. The constrained object must satisfy at least one. A branch must be non-empty: an empty branch is satisfied by every object and neutralizes the alternation. - """ - - @field_validator("required", mode="after") - def _enforce_unique_items_required(cls, value): # noqa: N805 - """JSON Schema uniqueItems: reject duplicate entries.""" - if value is None: - return value - seen = [] - for item in value: - if item in seen: - raise ValueError( - "Items must be unique (schema uniqueItems=true)" - ) - seen.append(item) - return value diff --git a/src/ucp_sdk/models/schemas/common/types/reverse_domain_name.py b/src/ucp_sdk/models/schemas/common/types/reverse_domain_name.py deleted file mode 100644 index 733442e..0000000 --- a/src/ucp_sdk/models/schemas/common/types/reverse_domain_name.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated - -from pydantic import Field -from typing_extensions import TypeAliasType - -ReverseDomainName = TypeAliasType( - "ReverseDomainName", - Annotated[ - str, - Field( - ..., - examples=[ - "dev.ucp.shopping.checkout", - "dev.ucp.common.identity_linking", - "com.example.loyalty_gold", - "com.example-shop.checkout", - "com.2example.cart", - "uk.co.example-shop.checkout", - "xn--p1ai.example.checkout", - ], - pattern="^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?)+$", - title="Reverse Domain Name", - ), - ], -) -""" -Reverse-domain identifier used for collision-safe namespacing of capabilities, services, handlers, eligibility claims, and extension-contributed keys. Must contain at least two dot-separated segments (e.g., 'dev.ucp.shopping.checkout', 'com.example.loyalty_gold'). Segments after the first are domain- or identifier-derived: they may contain interior hyphens, may start with a digit, and may contain underscores (e.g., 'com.example-shop.checkout', 'com.2example.cart', 'dev.ucp.common.identity_linking'), but must not start or end with a hyphen. The first segment (the reversed top-level domain) is letters and digits, and may contain interior hyphens to support internationalized (punycode) top-level domains such as 'xn--p1ai'. -""" diff --git a/src/ucp_sdk/models/schemas/common/types/reverse_domain_name_create_request.py b/src/ucp_sdk/models/schemas/common/types/reverse_domain_name_create_request.py deleted file mode 100644 index b8ae8ba..0000000 --- a/src/ucp_sdk/models/schemas/common/types/reverse_domain_name_create_request.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated - -from pydantic import Field -from typing_extensions import TypeAliasType - -ReverseDomainNameCreateRequest = TypeAliasType( - "ReverseDomainNameCreateRequest", - Annotated[ - str, - Field( - ..., - examples=[ - "dev.ucp.shopping.checkout", - "dev.ucp.common.identity_linking", - "com.example.loyalty_gold", - "com.example-shop.checkout", - "com.2example.cart", - "uk.co.example-shop.checkout", - "xn--p1ai.example.checkout", - ], - pattern="^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?)+$", - title="Reverse Domain Name Create Request", - ), - ], -) -""" -Reverse-domain identifier used for collision-safe namespacing of capabilities, services, handlers, eligibility claims, and extension-contributed keys. Must contain at least two dot-separated segments (e.g., 'dev.ucp.shopping.checkout', 'com.example.loyalty_gold'). Segments after the first are domain- or identifier-derived: they may contain interior hyphens, may start with a digit, and may contain underscores (e.g., 'com.example-shop.checkout', 'com.2example.cart', 'dev.ucp.common.identity_linking'), but must not start or end with a hyphen. The first segment (the reversed top-level domain) is letters and digits, and may contain interior hyphens to support internationalized (punycode) top-level domains such as 'xn--p1ai'. -""" diff --git a/src/ucp_sdk/models/schemas/common/types/reverse_domain_name_update_request.py b/src/ucp_sdk/models/schemas/common/types/reverse_domain_name_update_request.py deleted file mode 100644 index f0beeca..0000000 --- a/src/ucp_sdk/models/schemas/common/types/reverse_domain_name_update_request.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated - -from pydantic import Field -from typing_extensions import TypeAliasType - -ReverseDomainNameUpdateRequest = TypeAliasType( - "ReverseDomainNameUpdateRequest", - Annotated[ - str, - Field( - ..., - examples=[ - "dev.ucp.shopping.checkout", - "dev.ucp.common.identity_linking", - "com.example.loyalty_gold", - "com.example-shop.checkout", - "com.2example.cart", - "uk.co.example-shop.checkout", - "xn--p1ai.example.checkout", - ], - pattern="^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?)+$", - title="Reverse Domain Name Update Request", - ), - ], -) -""" -Reverse-domain identifier used for collision-safe namespacing of capabilities, services, handlers, eligibility claims, and extension-contributed keys. Must contain at least two dot-separated segments (e.g., 'dev.ucp.shopping.checkout', 'com.example.loyalty_gold'). Segments after the first are domain- or identifier-derived: they may contain interior hyphens, may start with a digit, and may contain underscores (e.g., 'com.example-shop.checkout', 'com.2example.cart', 'dev.ucp.common.identity_linking'), but must not start or end with a hyphen. The first segment (the reversed top-level domain) is letters and digits, and may contain interior hyphens to support internationalized (punycode) top-level domains such as 'xn--p1ai'. -""" diff --git a/src/ucp_sdk/models/schemas/common/types/signals.py b/src/ucp_sdk/models/schemas/common/types/signals.py index 7176f00..1ac53cc 100644 --- a/src/ucp_sdk/models/schemas/common/types/signals.py +++ b/src/ucp_sdk/models/schemas/common/types/signals.py @@ -1,54 +1,7 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Signals models.""" from __future__ import annotations -import re - -from pydantic import BaseModel, ConfigDict, Field, model_validator - - -class Signals(BaseModel): - """ - Environment data provided by the platform to support authorization and abuse prevention. Values MUST NOT be buyer-asserted claims — platforms provide signals based on direct observation or independently verifiable third-party attestations. All signal keys MUST use reverse-domain naming to ensure provenance and prevent collisions when multiple extensions contribute to the shared namespace. - """ - - model_config = ConfigDict( - extra="allow", - ) - dev_ucp_buyer_ip: str | None = Field(None, alias="dev.ucp.buyer_ip") - """ - Client's IP address (IPv4 or IPv6). - """ - dev_ucp_user_agent: str | None = Field(None, alias="dev.ucp.user_agent") - """ - Client's HTTP User-Agent header or equivalent. - """ +from ...models import Signals - @model_validator(mode="after") - def _enforce_property_names(self): - """JSON Schema propertyNames: every extra key must match the - declared reverse-domain pattern (schema propertyNames).""" - pattern = "^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?)+$" - for key in self.model_extra or {}: - if re.fullmatch(pattern, key) is None: - raise ValueError( - f"Property name {key!r} does not match the schema " - f"propertyNames pattern {pattern}" - ) - return self +__all__ = ["Signals"] diff --git a/src/ucp_sdk/models/schemas/common/types/signals_complete_request.py b/src/ucp_sdk/models/schemas/common/types/signals_complete_request.py deleted file mode 100644 index 0e46c33..0000000 --- a/src/ucp_sdk/models/schemas/common/types/signals_complete_request.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -import re - -from pydantic import BaseModel, ConfigDict, Field, model_validator - - -class SignalsCompleteRequest(BaseModel): - """ - Environment data provided by the platform to support authorization and abuse prevention. Values MUST NOT be buyer-asserted claims — platforms provide signals based on direct observation or independently verifiable third-party attestations. All signal keys MUST use reverse-domain naming to ensure provenance and prevent collisions when multiple extensions contribute to the shared namespace. - """ - - model_config = ConfigDict( - extra="allow", - ) - dev_ucp_buyer_ip: str | None = Field(None, alias="dev.ucp.buyer_ip") - """ - Client's IP address (IPv4 or IPv6). - """ - dev_ucp_user_agent: str | None = Field(None, alias="dev.ucp.user_agent") - """ - Client's HTTP User-Agent header or equivalent. - """ - - @model_validator(mode="after") - def _enforce_property_names(self): - """JSON Schema propertyNames: every extra key must match the - declared reverse-domain pattern (schema propertyNames).""" - pattern = "^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?)+$" - for key in self.model_extra or {}: - if re.fullmatch(pattern, key) is None: - raise ValueError( - f"Property name {key!r} does not match the schema " - f"propertyNames pattern {pattern}" - ) - return self diff --git a/src/ucp_sdk/models/schemas/common/types/signals_create_request.py b/src/ucp_sdk/models/schemas/common/types/signals_create_request.py deleted file mode 100644 index 6857861..0000000 --- a/src/ucp_sdk/models/schemas/common/types/signals_create_request.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -import re - -from pydantic import BaseModel, ConfigDict, Field, model_validator - - -class SignalsCreateRequest(BaseModel): - """ - Environment data provided by the platform to support authorization and abuse prevention. Values MUST NOT be buyer-asserted claims — platforms provide signals based on direct observation or independently verifiable third-party attestations. All signal keys MUST use reverse-domain naming to ensure provenance and prevent collisions when multiple extensions contribute to the shared namespace. - """ - - model_config = ConfigDict( - extra="allow", - ) - dev_ucp_buyer_ip: str | None = Field(None, alias="dev.ucp.buyer_ip") - """ - Client's IP address (IPv4 or IPv6). - """ - dev_ucp_user_agent: str | None = Field(None, alias="dev.ucp.user_agent") - """ - Client's HTTP User-Agent header or equivalent. - """ - - @model_validator(mode="after") - def _enforce_property_names(self): - """JSON Schema propertyNames: every extra key must match the - declared reverse-domain pattern (schema propertyNames).""" - pattern = "^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?)+$" - for key in self.model_extra or {}: - if re.fullmatch(pattern, key) is None: - raise ValueError( - f"Property name {key!r} does not match the schema " - f"propertyNames pattern {pattern}" - ) - return self diff --git a/src/ucp_sdk/models/schemas/common/types/signals_update_request.py b/src/ucp_sdk/models/schemas/common/types/signals_update_request.py deleted file mode 100644 index 6d6a714..0000000 --- a/src/ucp_sdk/models/schemas/common/types/signals_update_request.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -import re - -from pydantic import BaseModel, ConfigDict, Field, model_validator - - -class SignalsUpdateRequest(BaseModel): - """ - Environment data provided by the platform to support authorization and abuse prevention. Values MUST NOT be buyer-asserted claims — platforms provide signals based on direct observation or independently verifiable third-party attestations. All signal keys MUST use reverse-domain naming to ensure provenance and prevent collisions when multiple extensions contribute to the shared namespace. - """ - - model_config = ConfigDict( - extra="allow", - ) - dev_ucp_buyer_ip: str | None = Field(None, alias="dev.ucp.buyer_ip") - """ - Client's IP address (IPv4 or IPv6). - """ - dev_ucp_user_agent: str | None = Field(None, alias="dev.ucp.user_agent") - """ - Client's HTTP User-Agent header or equivalent. - """ - - @model_validator(mode="after") - def _enforce_property_names(self): - """JSON Schema propertyNames: every extra key must match the - declared reverse-domain pattern (schema propertyNames).""" - pattern = "^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?)+$" - for key in self.model_extra or {}: - if re.fullmatch(pattern, key) is None: - raise ValueError( - f"Property name {key!r} does not match the schema " - f"propertyNames pattern {pattern}" - ) - return self diff --git a/src/ucp_sdk/models/schemas/common/types/signed_amount.py b/src/ucp_sdk/models/schemas/common/types/signed_amount.py deleted file mode 100644 index 95a93cd..0000000 --- a/src/ucp_sdk/models/schemas/common/types/signed_amount.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated - -from pydantic import Field -from typing_extensions import TypeAliasType - -SignedAmount = TypeAliasType( - "SignedAmount", - Annotated[ - int, - Field( - ..., - ge=-9007199254740991, - le=9007199254740991, - title="Signed Amount", - ), - ], -) -""" -Monetary amount in the currency's minor unit as defined by ISO 4217. Refer to the currency's exponent to determine minor-to-major ratio (e.g., 2 for USD, 0 for JPY, 3 for KWD). May be negative — the sign is intrinsic to the value (e.g., discounts are negative, charges are positive). -""" diff --git a/src/ucp_sdk/models/schemas/common/types/signed_amount_create_request.py b/src/ucp_sdk/models/schemas/common/types/signed_amount_create_request.py deleted file mode 100644 index baec495..0000000 --- a/src/ucp_sdk/models/schemas/common/types/signed_amount_create_request.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated - -from pydantic import Field -from typing_extensions import TypeAliasType - -SignedAmountCreateRequest = TypeAliasType( - "SignedAmountCreateRequest", - Annotated[ - int, - Field( - ..., - ge=-9007199254740991, - le=9007199254740991, - title="Signed Amount Create Request", - ), - ], -) -""" -Monetary amount in the currency's minor unit as defined by ISO 4217. Refer to the currency's exponent to determine minor-to-major ratio (e.g., 2 for USD, 0 for JPY, 3 for KWD). May be negative — the sign is intrinsic to the value (e.g., discounts are negative, charges are positive). -""" diff --git a/src/ucp_sdk/models/schemas/common/types/signed_amount_update_request.py b/src/ucp_sdk/models/schemas/common/types/signed_amount_update_request.py deleted file mode 100644 index 3e09efe..0000000 --- a/src/ucp_sdk/models/schemas/common/types/signed_amount_update_request.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated - -from pydantic import Field -from typing_extensions import TypeAliasType - -SignedAmountUpdateRequest = TypeAliasType( - "SignedAmountUpdateRequest", - Annotated[ - int, - Field( - ..., - ge=-9007199254740991, - le=9007199254740991, - title="Signed Amount Update Request", - ), - ], -) -""" -Monetary amount in the currency's minor unit as defined by ISO 4217. Refer to the currency's exponent to determine minor-to-major ratio (e.g., 2 for USD, 0 for JPY, 3 for KWD). May be negative — the sign is intrinsic to the value (e.g., discounts are negative, charges are positive). -""" diff --git a/src/ucp_sdk/models/schemas/common/types/time_interval.py b/src/ucp_sdk/models/schemas/common/types/time_interval.py deleted file mode 100644 index 00a1e66..0000000 --- a/src/ucp_sdk/models/schemas/common/types/time_interval.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field, model_validator - - -class TimeInterval(BaseModel): - """ - Reusable opening and closing time fields for a containing schedule schema. Containing schemas determine whether the `opens` and `closes` pair is required; this fragment's standalone `{}` is not an interval. - """ - - model_config = ConfigDict( - extra="allow", - ) - opens: str | None = Field(None, pattern="^([01][0-9]|2[0-3]):[0-5][0-9]$") - """ - Opening time in 24-hour HH:MM format. - """ - closes: str | None = Field(None, pattern="^([01][0-9]|2[0-3]):[0-5][0-9]$") - """ - Closing time in 24-hour HH:MM format. - """ - - @model_validator(mode="after") - def _enforce_dependent_required(self): - """JSON Schema dependentRequired: enforce dependent fields.""" - rules = {"opens": ["closes"], "closes": ["opens"]} - provided = self.model_fields_set | set(self.model_extra or {}) - for field, required_fields in rules.items(): - if field not in provided: - continue - for required in required_fields: - if required not in provided: - raise ValueError( - f"Field {required!r} is required when {field!r} " - "is provided (schema dependentRequired)" - ) - return self diff --git a/src/ucp_sdk/models/schemas/common/types/time_interval_create_request.py b/src/ucp_sdk/models/schemas/common/types/time_interval_create_request.py deleted file mode 100644 index b6d9a5b..0000000 --- a/src/ucp_sdk/models/schemas/common/types/time_interval_create_request.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class TimeIntervalCreateRequest(BaseModel): - """ - Reusable opening and closing time fields for a containing schedule schema. Containing schemas determine whether the `opens` and `closes` pair is required; this fragment's standalone `{}` is not an interval. - """ - - model_config = ConfigDict( - extra="allow", - ) diff --git a/src/ucp_sdk/models/schemas/common/types/time_interval_update_request.py b/src/ucp_sdk/models/schemas/common/types/time_interval_update_request.py deleted file mode 100644 index 907d22e..0000000 --- a/src/ucp_sdk/models/schemas/common/types/time_interval_update_request.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class TimeIntervalUpdateRequest(BaseModel): - """ - Reusable opening and closing time fields for a containing schedule schema. Containing schemas determine whether the `opens` and `closes` pair is required; this fragment's standalone `{}` is not an interval. - """ - - model_config = ConfigDict( - extra="allow", - ) diff --git a/src/ucp_sdk/models/schemas/common/types/token_credential.py b/src/ucp_sdk/models/schemas/common/types/token_credential.py deleted file mode 100644 index 596f097..0000000 --- a/src/ucp_sdk/models/schemas/common/types/token_credential.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import ConfigDict - -from .payment_credential import PaymentCredential - - -class TokenCredential(PaymentCredential): - """ - Base token credential schema. Concrete payment handlers may extend this schema with additional fields and define their own constraints. - """ - - model_config = ConfigDict( - extra="allow", - ) - type: str - """ - The specific type of token produced by the handler (e.g., 'stripe_token'). - """ - token: str - """ - The token value. - """ diff --git a/src/ucp_sdk/models/schemas/common/types/total.py b/src/ucp_sdk/models/schemas/common/types/total.py deleted file mode 100644 index d4c4c6e..0000000 --- a/src/ucp_sdk/models/schemas/common/types/total.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -import operator - -from pydantic import BaseModel, ConfigDict, model_validator - -from . import signed_amount - - -class Total(BaseModel): - """ - A cost breakdown entry with a category, amount, and optional display text. - """ - - model_config = ConfigDict( - extra="allow", - ) - type: str - """ - Cost category. Well-known values: subtotal, items_discount, discount, fulfillment, tax, fee, total. Businesses MAY use additional values. - """ - display_text: str | None = None - """ - Text to display against the amount. Should reflect appropriate method (e.g., 'Shipping', 'Delivery'). - """ - amount: signed_amount.SignedAmount - - @model_validator(mode="after") - def _enforce_conditional_bounds(self): - """JSON Schema if/then: enforce conditional numeric bounds.""" - rules = [ - { - "discriminator": "type", - "values": ["discount", "items_discount"], - "bounds": {"amount": {"exclusiveMaximum": 0}}, - }, - { - "discriminator": "type", - "values": ["subtotal", "fulfillment", "tax", "fee"], - "bounds": {"amount": {"minimum": 0}}, - }, - ] - checks = { - "minimum": (">=", "lt"), - "maximum": ("<=", "gt"), - "exclusiveMinimum": (">", "le"), - "exclusiveMaximum": ("<", "ge"), - "const": ("==", "ne"), - } - for rule in rules: - actual = getattr(self, rule["discriminator"], None) - if actual not in rule["values"]: - continue - for field, bounds in rule["bounds"].items(): - value = getattr(self, field, None) - if value is None: - continue - for keyword, limit in bounds.items(): - symbol, op_name = checks[keyword] - if getattr(operator, op_name)(value, limit): - raise ValueError( - f"Field {field!r} must be {symbol} {limit} " - f"when {rule['discriminator']} is {actual!r}" - ) - return self diff --git a/src/ucp_sdk/models/schemas/common/types/total_create_request.py b/src/ucp_sdk/models/schemas/common/types/total_create_request.py deleted file mode 100644 index e3eb186..0000000 --- a/src/ucp_sdk/models/schemas/common/types/total_create_request.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class TotalCreateRequest(BaseModel): - """ - A cost breakdown entry with a category, amount, and optional display text. - """ - - model_config = ConfigDict( - extra="allow", - ) diff --git a/src/ucp_sdk/models/schemas/common/types/total_update_request.py b/src/ucp_sdk/models/schemas/common/types/total_update_request.py deleted file mode 100644 index 0f58f46..0000000 --- a/src/ucp_sdk/models/schemas/common/types/total_update_request.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class TotalUpdateRequest(BaseModel): - """ - A cost breakdown entry with a category, amount, and optional display text. - """ - - model_config = ConfigDict( - extra="allow", - ) diff --git a/src/ucp_sdk/models/schemas/common/types/totals.py b/src/ucp_sdk/models/schemas/common/types/totals.py index bba32a9..4911a96 100644 --- a/src/ucp_sdk/models/schemas/common/types/totals.py +++ b/src/ucp_sdk/models/schemas/common/types/totals.py @@ -1,181 +1,21 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Totals models.""" from __future__ import annotations -import operator - -from typing import Annotated - -from pydantic import ( - BaseModel, - ConfigDict, - Field, - AfterValidator, - model_validator, +from ...models import ( + Total, + TotalCreateRequest, + Totals, + TotalsCreateRequest, + TotalsUpdateRequest, + TotalUpdateRequest, ) -from typing_extensions import TypeAliasType - -from . import signed_amount -from .total import Total as Total_1 - - -class Line(BaseModel): - """ - Sub-line entry. Additional metadata MAY be included. - """ - - model_config = ConfigDict( - extra="allow", - ) - display_text: str - """ - Human-readable label for this sub-line. - """ - amount: signed_amount.SignedAmount - -class Total(Total_1): - model_config = ConfigDict( - extra="allow", - ) - lines: list[Line] | None = None - """ - Optional itemized breakdown. The parent entry is always rendered; lines are supplementary. Sum of line amounts MUST equal the parent entry amount. - """ - - @model_validator(mode="after") - def _enforce_conditional_bounds(self): - """JSON Schema if/then: enforce conditional numeric bounds.""" - rules = [ - { - "discriminator": "type", - "values": ["discount", "items_discount"], - "bounds": {"amount": {"exclusiveMaximum": 0}}, - }, - { - "discriminator": "type", - "values": ["subtotal", "fulfillment", "tax", "fee"], - "bounds": {"amount": {"minimum": 0}}, - }, - ] - checks = { - "minimum": (">=", "lt"), - "maximum": ("<=", "gt"), - "exclusiveMinimum": (">", "le"), - "exclusiveMaximum": ("<", "ge"), - "const": ("==", "ne"), - } - for rule in rules: - actual = getattr(self, rule["discriminator"], None) - if actual not in rule["values"]: - continue - for field, bounds in rule["bounds"].items(): - value = getattr(self, field, None) - if value is None: - continue - for keyword, limit in bounds.items(): - symbol, op_name = checks[keyword] - if getattr(operator, op_name)(value, limit): - raise ValueError( - f"Field {field!r} must be {symbol} {limit} " - f"when {rule['discriminator']} is {actual!r}" - ) - return self - - -def _enforce_contains_totals(value): - """JSON Schema contains/minContains/maxContains (see #49).""" - _matched_0 = sum( - 1 - for _item in value - if ( - _item.get("type") - if isinstance(_item, dict) - else getattr(_item, "type", None) - ) - == "subtotal" - ) - if _matched_0 < 1: - raise ValueError( - "Array must contain at least 1 entry " - "matching type=='subtotal' (schema minContains=1)" - ) - if _matched_0 > 1: - raise ValueError( - "Array must contain at most 1 entry " - "matching type=='subtotal' (schema maxContains=1)" - ) - _matched_1 = sum( - 1 - for _item in value - if ( - _item.get("type") - if isinstance(_item, dict) - else getattr(_item, "type", None) - ) - == "total" - ) - if _matched_1 < 1: - raise ValueError( - "Array must contain at least 1 entry " - "matching type=='total' (schema minContains=1)" - ) - if _matched_1 > 1: - raise ValueError( - "Array must contain at most 1 entry " - "matching type=='total' (schema maxContains=1)" - ) - _excluded = [ - "subtotal", - "items_discount", - "discount", - "fulfillment", - "tax", - "fee", - "total", - ] - for _item in value: - _actual = ( - _item.get("type") - if isinstance(_item, dict) - else getattr(_item, "type", None) - ) - if _actual in _excluded: - continue - if isinstance(_item, dict) and "display_text" not in _item: - raise ValueError("Field 'display_text' is required for custom type") - if ( - not isinstance(_item, dict) - and "display_text" not in _item.model_fields_set - ): - raise ValueError("Field 'display_text' is required for custom type") - return value - - -Totals = TypeAliasType( +__all__ = [ "Totals", - Annotated[ - list[Total], - Field(..., title="Totals"), - AfterValidator(_enforce_contains_totals), - ], -) -""" -Pricing breakdown provided by the business. MUST contain exactly one subtotal and one total entry. Detail types (tax, fee, discount, fulfillment) may appear multiple times for itemization. Platforms MUST render all entries in order using display_text and amount. -""" + "TotalsCreateRequest", + "TotalsUpdateRequest", + "Total", + "TotalCreateRequest", + "TotalUpdateRequest", +] diff --git a/src/ucp_sdk/models/schemas/common/types/totals_create_request.py b/src/ucp_sdk/models/schemas/common/types/totals_create_request.py deleted file mode 100644 index e2f89df..0000000 --- a/src/ucp_sdk/models/schemas/common/types/totals_create_request.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated - -from pydantic import Field, AfterValidator -from typing_extensions import TypeAliasType - -from . import total_create_request - - -def _enforce_contains_totals_create_request(value): - """JSON Schema contains/minContains/maxContains (see #49).""" - _matched_0 = sum( - 1 - for _item in value - if ( - _item.get("type") - if isinstance(_item, dict) - else getattr(_item, "type", None) - ) - == "subtotal" - ) - if _matched_0 < 1: - raise ValueError( - "Array must contain at least 1 entry " - "matching type=='subtotal' (schema minContains=1)" - ) - if _matched_0 > 1: - raise ValueError( - "Array must contain at most 1 entry " - "matching type=='subtotal' (schema maxContains=1)" - ) - _matched_1 = sum( - 1 - for _item in value - if ( - _item.get("type") - if isinstance(_item, dict) - else getattr(_item, "type", None) - ) - == "total" - ) - if _matched_1 < 1: - raise ValueError( - "Array must contain at least 1 entry " - "matching type=='total' (schema minContains=1)" - ) - if _matched_1 > 1: - raise ValueError( - "Array must contain at most 1 entry " - "matching type=='total' (schema maxContains=1)" - ) - _excluded = [ - "subtotal", - "items_discount", - "discount", - "fulfillment", - "tax", - "fee", - "total", - ] - for _item in value: - _actual = ( - _item.get("type") - if isinstance(_item, dict) - else getattr(_item, "type", None) - ) - if _actual in _excluded: - continue - if isinstance(_item, dict) and "display_text" not in _item: - raise ValueError("Field 'display_text' is required for custom type") - if ( - not isinstance(_item, dict) - and "display_text" not in _item.model_fields_set - ): - raise ValueError("Field 'display_text' is required for custom type") - return value - - -TotalsCreateRequest = TypeAliasType( - "TotalsCreateRequest", - Annotated[ - list[total_create_request.TotalCreateRequest], - Field(..., title="Totals Create Request"), - AfterValidator(_enforce_contains_totals_create_request), - ], -) -""" -Pricing breakdown provided by the business. MUST contain exactly one subtotal and one total entry. Detail types (tax, fee, discount, fulfillment) may appear multiple times for itemization. Platforms MUST render all entries in order using display_text and amount. -""" diff --git a/src/ucp_sdk/models/schemas/common/types/totals_update_request.py b/src/ucp_sdk/models/schemas/common/types/totals_update_request.py deleted file mode 100644 index 15eba47..0000000 --- a/src/ucp_sdk/models/schemas/common/types/totals_update_request.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated - -from pydantic import Field, AfterValidator -from typing_extensions import TypeAliasType - -from . import total_update_request - - -def _enforce_contains_totals_update_request(value): - """JSON Schema contains/minContains/maxContains (see #49).""" - _matched_0 = sum( - 1 - for _item in value - if ( - _item.get("type") - if isinstance(_item, dict) - else getattr(_item, "type", None) - ) - == "subtotal" - ) - if _matched_0 < 1: - raise ValueError( - "Array must contain at least 1 entry " - "matching type=='subtotal' (schema minContains=1)" - ) - if _matched_0 > 1: - raise ValueError( - "Array must contain at most 1 entry " - "matching type=='subtotal' (schema maxContains=1)" - ) - _matched_1 = sum( - 1 - for _item in value - if ( - _item.get("type") - if isinstance(_item, dict) - else getattr(_item, "type", None) - ) - == "total" - ) - if _matched_1 < 1: - raise ValueError( - "Array must contain at least 1 entry " - "matching type=='total' (schema minContains=1)" - ) - if _matched_1 > 1: - raise ValueError( - "Array must contain at most 1 entry " - "matching type=='total' (schema maxContains=1)" - ) - _excluded = [ - "subtotal", - "items_discount", - "discount", - "fulfillment", - "tax", - "fee", - "total", - ] - for _item in value: - _actual = ( - _item.get("type") - if isinstance(_item, dict) - else getattr(_item, "type", None) - ) - if _actual in _excluded: - continue - if isinstance(_item, dict) and "display_text" not in _item: - raise ValueError("Field 'display_text' is required for custom type") - if ( - not isinstance(_item, dict) - and "display_text" not in _item.model_fields_set - ): - raise ValueError("Field 'display_text' is required for custom type") - return value - - -TotalsUpdateRequest = TypeAliasType( - "TotalsUpdateRequest", - Annotated[ - list[total_update_request.TotalUpdateRequest], - Field(..., title="Totals Update Request"), - AfterValidator(_enforce_contains_totals_update_request), - ], -) -""" -Pricing breakdown provided by the business. MUST contain exactly one subtotal and one total entry. Detail types (tax, fee, discount, fulfillment) may appear multiple times for itemization. Platforms MUST render all entries in order using display_text and amount. -""" diff --git a/src/ucp_sdk/models/schemas/common/types/unit.py b/src/ucp_sdk/models/schemas/common/types/unit.py index 478eccb..d1b2edc 100644 --- a/src/ucp_sdk/models/schemas/common/types/unit.py +++ b/src/ucp_sdk/models/schemas/common/types/unit.py @@ -1,79 +1,7 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Unit models.""" from __future__ import annotations -import operator - -from pydantic import BaseModel, ConfigDict, Field, model_validator - - -class Unit(BaseModel): - """ - A reusable unit descriptor for quantities and measures. Its unit-descriptor machine identity is (`unit`, effective `scale`), where effective `scale` is the provided `scale` or 0; `display_text` is excluded. - """ - - model_config = ConfigDict( - extra="allow", - ) - unit: str - """ - Stable machine identifier. The Business SHOULD use the exact UN/CEFACT Rec20 Common Code when one accurately identifies the unit. Otherwise, the Business MAY use a custom unit identifier and MUST use it consistently for the same unit. The Platform MUST treat an unrecognized identifier as opaque. - """ - scale: int | None = Field(0, ge=0, le=15) - """ - One step equals `10^-scale` of `unit`. When `unit` is `C62`, `scale`, if present, MUST be 0. The maximum of 15 is derived from the interoperable integer range: at scale 16 a single whole unit (10^16 steps) is no longer representable, so larger scales cannot denominate one unit of their own basis. Businesses needing finer granularity use a smaller unit. - """ - display_text: str - """ - Required printable unit label provided by the Business. The Platform MUST use it when it does not recognize `unit`; for a recognized UN/CEFACT Rec 20 Common Code, the Platform MAY substitute its own localized label. It does not participate in unit identity or mismatch comparison. - """ +from ...models import QuantityUnit, Unit, UnitPrice - @model_validator(mode="after") - def _enforce_conditional_bounds(self): - """JSON Schema if/then: enforce conditional numeric bounds.""" - rules = [ - { - "discriminator": "unit", - "values": ["C62"], - "bounds": {"scale": {"const": 0}}, - } - ] - checks = { - "minimum": (">=", "lt"), - "maximum": ("<=", "gt"), - "exclusiveMinimum": (">", "le"), - "exclusiveMaximum": ("<", "ge"), - "const": ("==", "ne"), - } - for rule in rules: - actual = getattr(self, rule["discriminator"], None) - if actual not in rule["values"]: - continue - for field, bounds in rule["bounds"].items(): - value = getattr(self, field, None) - if value is None: - continue - for keyword, limit in bounds.items(): - symbol, op_name = checks[keyword] - if getattr(operator, op_name)(value, limit): - raise ValueError( - f"Field {field!r} must be {symbol} {limit} " - f"when {rule['discriminator']} is {actual!r}" - ) - return self +__all__ = ["Unit", "QuantityUnit", "UnitPrice"] diff --git a/src/ucp_sdk/models/schemas/common/types/unit_create_request.py b/src/ucp_sdk/models/schemas/common/types/unit_create_request.py deleted file mode 100644 index 56b8475..0000000 --- a/src/ucp_sdk/models/schemas/common/types/unit_create_request.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -import operator - -from pydantic import BaseModel, ConfigDict, Field, model_validator - - -class UnitCreateRequest(BaseModel): - """ - A reusable unit descriptor for quantities and measures. Its unit-descriptor machine identity is (`unit`, effective `scale`), where effective `scale` is the provided `scale` or 0; `display_text` is excluded. - """ - - model_config = ConfigDict( - extra="allow", - ) - unit: str - """ - Stable machine identifier. The Business SHOULD use the exact UN/CEFACT Rec20 Common Code when one accurately identifies the unit. Otherwise, the Business MAY use a custom unit identifier and MUST use it consistently for the same unit. The Platform MUST treat an unrecognized identifier as opaque. - """ - scale: int | None = Field(0, ge=0, le=15) - """ - One step equals `10^-scale` of `unit`. When `unit` is `C62`, `scale`, if present, MUST be 0. The maximum of 15 is derived from the interoperable integer range: at scale 16 a single whole unit (10^16 steps) is no longer representable, so larger scales cannot denominate one unit of their own basis. Businesses needing finer granularity use a smaller unit. - """ - display_text: str - """ - Required printable unit label provided by the Business. The Platform MUST use it when it does not recognize `unit`; for a recognized UN/CEFACT Rec 20 Common Code, the Platform MAY substitute its own localized label. It does not participate in unit identity or mismatch comparison. - """ - - @model_validator(mode="after") - def _enforce_conditional_bounds(self): - """JSON Schema if/then: enforce conditional numeric bounds.""" - rules = [ - { - "discriminator": "unit", - "values": ["C62"], - "bounds": {"scale": {"const": 0}}, - } - ] - checks = { - "minimum": (">=", "lt"), - "maximum": ("<=", "gt"), - "exclusiveMinimum": (">", "le"), - "exclusiveMaximum": ("<", "ge"), - "const": ("==", "ne"), - } - for rule in rules: - actual = getattr(self, rule["discriminator"], None) - if actual not in rule["values"]: - continue - for field, bounds in rule["bounds"].items(): - value = getattr(self, field, None) - if value is None: - continue - for keyword, limit in bounds.items(): - symbol, op_name = checks[keyword] - if getattr(operator, op_name)(value, limit): - raise ValueError( - f"Field {field!r} must be {symbol} {limit} " - f"when {rule['discriminator']} is {actual!r}" - ) - return self diff --git a/src/ucp_sdk/models/schemas/common/types/unit_update_request.py b/src/ucp_sdk/models/schemas/common/types/unit_update_request.py deleted file mode 100644 index 1b52ca5..0000000 --- a/src/ucp_sdk/models/schemas/common/types/unit_update_request.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -import operator - -from pydantic import BaseModel, ConfigDict, Field, model_validator - - -class UnitUpdateRequest(BaseModel): - """ - A reusable unit descriptor for quantities and measures. Its unit-descriptor machine identity is (`unit`, effective `scale`), where effective `scale` is the provided `scale` or 0; `display_text` is excluded. - """ - - model_config = ConfigDict( - extra="allow", - ) - unit: str - """ - Stable machine identifier. The Business SHOULD use the exact UN/CEFACT Rec20 Common Code when one accurately identifies the unit. Otherwise, the Business MAY use a custom unit identifier and MUST use it consistently for the same unit. The Platform MUST treat an unrecognized identifier as opaque. - """ - scale: int | None = Field(0, ge=0, le=15) - """ - One step equals `10^-scale` of `unit`. When `unit` is `C62`, `scale`, if present, MUST be 0. The maximum of 15 is derived from the interoperable integer range: at scale 16 a single whole unit (10^16 steps) is no longer representable, so larger scales cannot denominate one unit of their own basis. Businesses needing finer granularity use a smaller unit. - """ - display_text: str - """ - Required printable unit label provided by the Business. The Platform MUST use it when it does not recognize `unit`; for a recognized UN/CEFACT Rec 20 Common Code, the Platform MAY substitute its own localized label. It does not participate in unit identity or mismatch comparison. - """ - - @model_validator(mode="after") - def _enforce_conditional_bounds(self): - """JSON Schema if/then: enforce conditional numeric bounds.""" - rules = [ - { - "discriminator": "unit", - "values": ["C62"], - "bounds": {"scale": {"const": 0}}, - } - ] - checks = { - "minimum": (">=", "lt"), - "maximum": ("<=", "gt"), - "exclusiveMinimum": (">", "le"), - "exclusiveMaximum": ("<", "ge"), - "const": ("==", "ne"), - } - for rule in rules: - actual = getattr(self, rule["discriminator"], None) - if actual not in rule["values"]: - continue - for field, bounds in rule["bounds"].items(): - value = getattr(self, field, None) - if value is None: - continue - for keyword, limit in bounds.items(): - symbol, op_name = checks[keyword] - if getattr(operator, op_name)(value, limit): - raise ValueError( - f"Field {field!r} must be {symbol} {limit} " - f"when {rule['discriminator']} is {actual!r}" - ) - return self diff --git a/src/ucp_sdk/models/schemas/common/types/warning_code.py b/src/ucp_sdk/models/schemas/common/types/warning_code.py deleted file mode 100644 index e14ff58..0000000 --- a/src/ucp_sdk/models/schemas/common/types/warning_code.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated - -from pydantic import Field -from typing_extensions import TypeAliasType - -WarningCode = TypeAliasType( - "WarningCode", - Annotated[ - str, - Field( - ..., - examples=[ - "final_sale", - "prop65", - "fulfillment_changed", - "payment_term_changed", - "age_restricted", - ], - title="Warning Code", - ), - ], -) -""" -Warning code identifying the type of warning. Standard codes are defined in capability specifications (see examples) and have standardized semantics; freeform codes are permitted. -""" diff --git a/src/ucp_sdk/models/schemas/common/types/warning_code_create_request.py b/src/ucp_sdk/models/schemas/common/types/warning_code_create_request.py deleted file mode 100644 index 27d72d9..0000000 --- a/src/ucp_sdk/models/schemas/common/types/warning_code_create_request.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated - -from pydantic import Field -from typing_extensions import TypeAliasType - -WarningCodeCreateRequest = TypeAliasType( - "WarningCodeCreateRequest", - Annotated[ - str, - Field( - ..., - examples=[ - "final_sale", - "prop65", - "fulfillment_changed", - "payment_term_changed", - "age_restricted", - ], - title="Warning Code Create Request", - ), - ], -) -""" -Warning code identifying the type of warning. Standard codes are defined in capability specifications (see examples) and have standardized semantics; freeform codes are permitted. -""" diff --git a/src/ucp_sdk/models/schemas/common/types/warning_code_update_request.py b/src/ucp_sdk/models/schemas/common/types/warning_code_update_request.py deleted file mode 100644 index e9cb732..0000000 --- a/src/ucp_sdk/models/schemas/common/types/warning_code_update_request.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated - -from pydantic import Field -from typing_extensions import TypeAliasType - -WarningCodeUpdateRequest = TypeAliasType( - "WarningCodeUpdateRequest", - Annotated[ - str, - Field( - ..., - examples=[ - "final_sale", - "prop65", - "fulfillment_changed", - "payment_term_changed", - "age_restricted", - ], - title="Warning Code Update Request", - ), - ], -) -""" -Warning code identifying the type of warning. Standard codes are defined in capability specifications (see examples) and have standardized semantics; freeform codes are permitted. -""" diff --git a/src/ucp_sdk/models/schemas/payment_handler.py b/src/ucp_sdk/models/schemas/payment_handler.py index bf9ffe4..deede3f 100644 --- a/src/ucp_sdk/models/schemas/payment_handler.py +++ b/src/ucp_sdk/models/schemas/payment_handler.py @@ -1,173 +1,19 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Payment handler models.""" from __future__ import annotations -from typing import Annotated, Any - -from pydantic import AnyUrl, BaseModel, ConfigDict, Field -from typing_extensions import TypeAliasType - -from .common.types import available_payment_instrument - -PaymentHandler = TypeAliasType( - "PaymentHandler", Annotated[Any, Field(..., title="Payment Handler")] +from .models import ( + PaymentHandlerBase, + PaymentHandlerBusinessSchema, + PaymentHandlerPlatformSchema, + PaymentHandlerResponseSchema, ) -""" -Schema for UCP payment handlers. Handlers define how payment instruments are processed. -""" - - -class Base(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - version: str = Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$") - """ - Entity version in YYYY-MM-DD format. - """ - spec: AnyUrl | None = None - """ - URL to human-readable specification document. - """ - schema_: AnyUrl | None = Field(None, alias="schema") - """ - URL to JSON Schema defining this entity's structure and payloads. - """ - id: str - """ - Unique identifier for this entity instance. Used to disambiguate when multiple instances exist. - """ - config: dict[str, Any] | None = None - """ - Entity-specific configuration. Structure defined by each entity's schema. - """ - available_instruments: ( - list[available_payment_instrument.AvailablePaymentInstrument] | None - ) = Field(None, min_length=1) - """ - Instrument types this handler supports, with optional constraints. When absent, every instrument should be considered available. - """ - - -class PlatformSchema(BaseModel): - """ - Platform declaration for discovery profiles. May include partial config state required for discovery. - """ - - model_config = ConfigDict( - extra="allow", - ) - version: str = Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$") - """ - Entity version in YYYY-MM-DD format. - """ - spec: AnyUrl - """ - URL to human-readable specification document. - """ - schema_: AnyUrl = Field(..., alias="schema") - """ - URL to JSON Schema defining this entity's structure and payloads. - """ - id: str - """ - Unique identifier for this entity instance. Used to disambiguate when multiple instances exist. - """ - config: dict[str, Any] | None = None - """ - Entity-specific configuration. Structure defined by each entity's schema. - """ - available_instruments: ( - list[available_payment_instrument.AvailablePaymentInstrument] | None - ) = Field(None, min_length=1) - """ - Instrument types this handler supports, with optional constraints. When absent, every instrument should be considered available. - """ - - -class BusinessSchema(BaseModel): - """ - Business declaration for discovery profiles. May include partial config state required for discovery. - """ - - model_config = ConfigDict( - extra="allow", - ) - version: str = Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$") - """ - Entity version in YYYY-MM-DD format. - """ - spec: AnyUrl | None = None - """ - URL to human-readable specification document. - """ - schema_: AnyUrl | None = Field(None, alias="schema") - """ - URL to JSON Schema defining this entity's structure and payloads. - """ - id: str - """ - Unique identifier for this entity instance. Used to disambiguate when multiple instances exist. - """ - config: dict[str, Any] | None = None - """ - Entity-specific configuration. Structure defined by each entity's schema. - """ - available_instruments: ( - list[available_payment_instrument.AvailablePaymentInstrument] | None - ) = Field(None, min_length=1) - """ - Instrument types this handler supports, with optional constraints. When absent, every instrument should be considered available. - """ - - -class ResponseSchema(BaseModel): - """ - Handler reference in responses. May include full config state for runtime usage of the handler. - """ - model_config = ConfigDict( - extra="allow", - ) - version: str = Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$") - """ - Entity version in YYYY-MM-DD format. - """ - spec: AnyUrl | None = None - """ - URL to human-readable specification document. - """ - schema_: AnyUrl | None = Field(None, alias="schema") - """ - URL to JSON Schema defining this entity's structure and payloads. - """ - id: str - """ - Unique identifier for this entity instance. Used to disambiguate when multiple instances exist. - """ - config: dict[str, Any] | None = None - """ - Entity-specific configuration. Structure defined by each entity's schema. - """ - available_instruments: ( - list[available_payment_instrument.AvailablePaymentInstrument] | None - ) = Field(None, min_length=1) - """ - Instrument types this handler supports, with optional constraints. When absent, every instrument should be considered available. - """ +Base = PaymentHandlerBase +__all__ = [ + "PaymentHandlerBase", + "PaymentHandlerBusinessSchema", + "PaymentHandlerPlatformSchema", + "PaymentHandlerResponseSchema", + "Base", +] diff --git a/src/ucp_sdk/models/schemas/profile.py b/src/ucp_sdk/models/schemas/profile.py index 91a2848..dc9eaf3 100644 --- a/src/ucp_sdk/models/schemas/profile.py +++ b/src/ucp_sdk/models/schemas/profile.py @@ -1,198 +1,7 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Profile models.""" from __future__ import annotations -import operator - -from pydantic import BaseModel, ConfigDict, Field, model_validator - -from . import ucp as ucp_1 - - -class JwkPublicKey(BaseModel): - """ - Public JSON Web Key used for HTTP Message Signatures and signed webhook verification. UCP profiles publish public keys only; private key material MUST NOT appear in a profile. Well-known key types: EC (ECDSA P-256, P-384) and OKP (EdDSA Ed25519); OKP keys are RECOMMENDED for signers opting into Web Bot Auth (WBA) interop on HTTP transport. A single profile MAY publish keys of either or both types; consumers select keys by kid. The kty, crv, and alg vocabularies are OPEN: verifiers MUST tolerate key types, curves, and algorithms they do not recognize, selecting keys by kid at verification time. An unsupported key affects only the signature that references it (algorithm_unsupported) and MUST NOT cause whole-profile rejection. Additional public JWK members are permitted; consumers ignore unknown members. - """ - - model_config = ConfigDict( - extra="allow", - ) - kid: str - """ - Key identifier referenced by Signature-Input keyid. For keys used in dual-audience (Web Bot Auth) signatures, the kid MUST be the key's JWK SHA-256 Thumbprint (RFC 7638) so UCP-Agent and Signature-Agent lookups resolve the same key; otherwise the kid MAY be any stable string. - """ - kty: str = Field(..., examples=["EC", "OKP"]) - """ - JWK key type. Well-known values: EC for ECDSA (P-256, P-384); OKP for EdDSA (Ed25519). Open vocabulary; verifiers tolerate unrecognized types and select keys by kid. - """ - crv: str | None = Field(None, examples=["P-256", "P-384", "Ed25519"]) - """ - Curve name. Well-known values: P-256, P-384 (EC); Ed25519 (OKP). Open vocabulary. - """ - x: str | None = None - """ - Public key value, base64url-encoded. For EC, the x coordinate (RFC 7518 §6.2); for OKP, the public key (RFC 8037 §2). - """ - y: str | None = None - """ - EC public key y coordinate, base64url-encoded (RFC 7518 §6.2). Not used by OKP keys. - """ - alg: str | None = Field(None, examples=["ES256", "ES384", "EdDSA"]) - """ - JWA algorithm associated with this public key. Optional; verifiers derive the algorithm from crv when alg is omitted. When present for a well-known curve it MUST match: ES256 with P-256, ES384 with P-384, EdDSA with Ed25519. - """ - use: str | None = None - """ - JWK public key use. UCP examples use sig for signatures. - """ - - @model_validator(mode="after") - def _enforce_conditional_required(self): - """JSON Schema if/then: enforce conditionally required fields.""" - rules = [ - { - "discriminator": "kty", - "values": ["EC"], - "required": ["crv", "x", "y"], - }, - { - "discriminator": "kty", - "values": ["OKP"], - "required": ["crv", "x"], - }, - ] - for rule in rules: - if getattr(self, rule["discriminator"], None) not in rule["values"]: - continue - for field in rule["required"]: - if field not in self.model_fields_set: - raise ValueError( - f"Field {field!r} is required by a schema condition" - ) - return self - - @model_validator(mode="after") - def _enforce_conditional_bounds(self): - """JSON Schema if/then: enforce conditional numeric bounds.""" - rules = [ - { - "discriminator": "crv", - "values": ["P-256"], - "bounds": {"alg": {"const": "ES256"}}, - }, - { - "discriminator": "crv", - "values": ["P-384"], - "bounds": {"alg": {"const": "ES384"}}, - }, - { - "discriminator": "crv", - "values": ["Ed25519"], - "bounds": {"alg": {"const": "EdDSA"}}, - }, - ] - checks = { - "minimum": (">=", "lt"), - "maximum": ("<=", "gt"), - "exclusiveMinimum": (">", "le"), - "exclusiveMaximum": ("<", "ge"), - "const": ("==", "ne"), - } - for rule in rules: - actual = getattr(self, rule["discriminator"], None) - if actual not in rule["values"]: - continue - for field, bounds in rule["bounds"].items(): - value = getattr(self, field, None) - if value is None: - continue - for keyword, limit in bounds.items(): - symbol, op_name = checks[keyword] - if getattr(operator, op_name)(value, limit): - raise ValueError( - f"Field {field!r} must be {symbol} {limit} " - f"when {rule['discriminator']} is {actual!r}" - ) - return self - - -class UcpProfileDocument(BaseModel): - """ - Variant-neutral wrapper schema for UCP profile documents. Use the business_schema definition to validate business profiles and the platform_schema definition to validate platform profiles. - """ - - model_config = ConfigDict( - extra="allow", - ) - ucp: ucp_1.Base - """ - Protocol metadata, capabilities, services, and payment handlers advertised by this party. - """ - keys: list[JwkPublicKey] | None = None - """ - Canonical UCP profile field for publishing signing keys, as a JWK Set per RFC 7517. When a profile publishes signing keys, they MUST appear here; this is where every UCP verifier reads them. Publishing keys[] makes the UCP profile a valid JWK Set that a signer can reuse as its Web Bot Auth key source: a WBA-shape verifier resolving via Signature-Agent type=jwks_uri pointed at this profile reads these keys, and the cimd and directory variants reach them through their own documents. See the Deployment Patterns for WBA Interop section in the overview for hosting patterns. - """ - - -class Base(BaseModel): - """ - Common wrapper for UCP profile documents. - """ - - model_config = ConfigDict( - extra="allow", - ) - ucp: ucp_1.Base - """ - Protocol metadata, capabilities, services, and payment handlers advertised by this party. - """ - keys: list[JwkPublicKey] | None = None - """ - Canonical UCP profile field for publishing signing keys, as a JWK Set per RFC 7517. When a profile publishes signing keys, they MUST appear here; this is where every UCP verifier reads them. Publishing keys[] makes the UCP profile a valid JWK Set that a signer can reuse as its Web Bot Auth key source: a WBA-shape verifier resolving via Signature-Agent type=jwks_uri pointed at this profile reads these keys, and the cimd and directory variants reach them through their own documents. See the Deployment Patterns for WBA Interop section in the overview for hosting patterns. - """ - - -class BusinessSchema(BaseModel): - """ - Profile document hosted by a business at /.well-known/ucp. - """ - - model_config = ConfigDict( - extra="allow", - ) - ucp: ucp_1.BusinessSchema - keys: list[JwkPublicKey] | None = None - """ - Canonical UCP profile field for publishing signing keys, as a JWK Set per RFC 7517. When a profile publishes signing keys, they MUST appear here; this is where every UCP verifier reads them. Publishing keys[] makes the UCP profile a valid JWK Set that a signer can reuse as its Web Bot Auth key source: a WBA-shape verifier resolving via Signature-Agent type=jwks_uri pointed at this profile reads these keys, and the cimd and directory variants reach them through their own documents. See the Deployment Patterns for WBA Interop section in the overview for hosting patterns. - """ - - -class PlatformSchema(BaseModel): - """ - Profile document hosted by a platform and advertised to businesses via UCP-Agent. - """ +from .models import JwkPublicKey, Profile, ProfileBase - model_config = ConfigDict( - extra="allow", - ) - ucp: ucp_1.PlatformSchema - keys: list[JwkPublicKey] | None = None - """ - Canonical UCP profile field for publishing signing keys, as a JWK Set per RFC 7517. When a profile publishes signing keys, they MUST appear here; this is where every UCP verifier reads them. Publishing keys[] makes the UCP profile a valid JWK Set that a signer can reuse as its Web Bot Auth key source: a WBA-shape verifier resolving via Signature-Agent type=jwks_uri pointed at this profile reads these keys, and the cimd and directory variants reach them through their own documents. See the Deployment Patterns for WBA Interop section in the overview for hosting patterns. - """ +__all__ = ["Profile", "ProfileBase", "JwkPublicKey"] diff --git a/src/ucp_sdk/models/schemas/service.py b/src/ucp_sdk/models/schemas/service.py index c7638e0..9f8ebe6 100644 --- a/src/ucp_sdk/models/schemas/service.py +++ b/src/ucp_sdk/models/schemas/service.py @@ -1,575 +1,19 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Service models.""" from __future__ import annotations -from typing import Annotated, Any, Literal - -from pydantic import AnyUrl, BaseModel, ConfigDict, Field -from typing_extensions import TypeAliasType - -UcpService = TypeAliasType( - "UcpService", Annotated[Any, Field(..., title="UCP Service")] -) -""" -Service declaration with one transport binding. Each transport binding is a separate entry in the service array; `version` identifies the service, not the transport. -""" - - -class Base(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - version: str = Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$") - """ - Entity version in YYYY-MM-DD format. - """ - spec: AnyUrl | None = None - """ - URL to human-readable specification document. - """ - schema_: AnyUrl | None = Field(None, alias="schema") - """ - URL to JSON Schema defining this entity's structure and payloads. - """ - id: str | None = None - """ - Unique identifier for this entity instance. Used to disambiguate when multiple instances exist. - """ - config: dict[str, Any] | None = None - """ - Entity-specific configuration. Structure defined by each entity's schema. - """ - transport: Literal["rest", "mcp", "a2a", "embedded"] - """ - Transport protocol for this service binding. - """ - endpoint: AnyUrl | None = None - """ - Endpoint URL for this transport binding. - """ - - -class PlatformSchema(BaseModel): - """ - Full service declaration for platform-level discovery. All transports require `version`, `spec`, and `transport`. REST, MCP, and embedded additionally require `schema`. - """ - - model_config = ConfigDict( - extra="allow", - ) - version: str = Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$") - """ - Entity version in YYYY-MM-DD format. - """ - spec: AnyUrl - """ - URL to human-readable specification document. - """ - schema_: AnyUrl = Field(..., alias="schema") - """ - URL to JSON Schema defining this entity's structure and payloads. - """ - id: str | None = None - """ - Unique identifier for this entity instance. Used to disambiguate when multiple instances exist. - """ - config: dict[str, Any] | None = None - """ - Entity-specific configuration. Structure defined by each entity's schema. - """ - transport: Literal["rest"] - """ - Transport protocol for this service binding. - """ - endpoint: AnyUrl | None = None - """ - Endpoint URL for this transport binding. - """ - - -class PlatformSchema8(BaseModel): - """ - Full service declaration for platform-level discovery. All transports require `version`, `spec`, and `transport`. REST, MCP, and embedded additionally require `schema`. - """ - - model_config = ConfigDict( - extra="allow", - ) - version: str = Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$") - """ - Entity version in YYYY-MM-DD format. - """ - spec: AnyUrl - """ - URL to human-readable specification document. - """ - schema_: AnyUrl = Field(..., alias="schema") - """ - URL to JSON Schema defining this entity's structure and payloads. - """ - id: str | None = None - """ - Unique identifier for this entity instance. Used to disambiguate when multiple instances exist. - """ - config: dict[str, Any] | None = None - """ - Entity-specific configuration. Structure defined by each entity's schema. - """ - transport: Literal["mcp"] - """ - Transport protocol for this service binding. - """ - endpoint: AnyUrl | None = None - """ - Endpoint URL for this transport binding. - """ - - -class PlatformSchema9(BaseModel): - """ - Full service declaration for platform-level discovery. All transports require `version`, `spec`, and `transport`. REST, MCP, and embedded additionally require `schema`. - """ - - model_config = ConfigDict( - extra="allow", - ) - version: str = Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$") - """ - Entity version in YYYY-MM-DD format. - """ - spec: AnyUrl - """ - URL to human-readable specification document. - """ - schema_: AnyUrl | None = Field(None, alias="schema") - """ - URL to JSON Schema defining this entity's structure and payloads. - """ - id: str | None = None - """ - Unique identifier for this entity instance. Used to disambiguate when multiple instances exist. - """ - config: dict[str, Any] | None = None - """ - Entity-specific configuration. Structure defined by each entity's schema. - """ - transport: Literal["a2a"] - """ - Transport protocol for this service binding. - """ - endpoint: AnyUrl | None = None - """ - Endpoint URL for this transport binding. - """ - - -class PlatformSchema10(BaseModel): - """ - Full service declaration for platform-level discovery. All transports require `version`, `spec`, and `transport`. REST, MCP, and embedded additionally require `schema`. - """ - - model_config = ConfigDict( - extra="allow", - ) - version: str = Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$") - """ - Entity version in YYYY-MM-DD format. - """ - spec: AnyUrl - """ - URL to human-readable specification document. - """ - schema_: AnyUrl = Field(..., alias="schema") - """ - URL to JSON Schema defining this entity's structure and payloads. - """ - id: str | None = None - """ - Unique identifier for this entity instance. Used to disambiguate when multiple instances exist. - """ - config: dict[str, Any] | None = None - """ - Entity-specific configuration. Structure defined by each entity's schema. - """ - transport: Literal["embedded"] - """ - Transport protocol for this service binding. - """ - endpoint: AnyUrl | None = None - """ - Endpoint URL for this transport binding. - """ - - -PlatformSchema6 = TypeAliasType( - "PlatformSchema6", - Annotated[ - PlatformSchema | PlatformSchema8 | PlatformSchema9 | PlatformSchema10, - Field(..., title="Service (Platform Schema)"), - ], -) -""" -Full service declaration for platform-level discovery. All transports require `version`, `spec`, and `transport`. REST, MCP, and embedded additionally require `schema`. -""" - - -class BusinessSchema(BaseModel): - """ - Service binding for business/merchant configuration. May override platform endpoints. - """ - - model_config = ConfigDict( - extra="allow", - ) - version: str = Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$") - """ - Entity version in YYYY-MM-DD format. - """ - spec: AnyUrl | None = None - """ - URL to human-readable specification document. - """ - schema_: AnyUrl | None = Field(None, alias="schema") - """ - URL to JSON Schema defining this entity's structure and payloads. - """ - id: str | None = None - """ - Unique identifier for this entity instance. Used to disambiguate when multiple instances exist. - """ - config: dict[str, Any] | None = None - """ - Entity-specific configuration. Structure defined by each entity's schema. - """ - transport: Literal["rest"] - """ - Transport protocol for this service binding. - """ - endpoint: AnyUrl - """ - Endpoint URL for this transport binding. - """ - - -class BusinessSchema5(BaseModel): - """ - Service binding for business/merchant configuration. May override platform endpoints. - """ - - model_config = ConfigDict( - extra="allow", - ) - version: str = Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$") - """ - Entity version in YYYY-MM-DD format. - """ - spec: AnyUrl | None = None - """ - URL to human-readable specification document. - """ - schema_: AnyUrl | None = Field(None, alias="schema") - """ - URL to JSON Schema defining this entity's structure and payloads. - """ - id: str | None = None - """ - Unique identifier for this entity instance. Used to disambiguate when multiple instances exist. - """ - config: dict[str, Any] | None = None - """ - Entity-specific configuration. Structure defined by each entity's schema. - """ - transport: Literal["mcp"] - """ - Transport protocol for this service binding. - """ - endpoint: AnyUrl - """ - Endpoint URL for this transport binding. - """ - - -class BusinessSchema6(BaseModel): - """ - Service binding for business/merchant configuration. May override platform endpoints. - """ - - model_config = ConfigDict( - extra="allow", - ) - version: str = Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$") - """ - Entity version in YYYY-MM-DD format. - """ - spec: AnyUrl | None = None - """ - URL to human-readable specification document. - """ - schema_: AnyUrl | None = Field(None, alias="schema") - """ - URL to JSON Schema defining this entity's structure and payloads. - """ - id: str | None = None - """ - Unique identifier for this entity instance. Used to disambiguate when multiple instances exist. - """ - config: dict[str, Any] | None = None - """ - Entity-specific configuration. Structure defined by each entity's schema. - """ - transport: Literal["a2a"] - """ - Transport protocol for this service binding. - """ - endpoint: AnyUrl - """ - Endpoint URL for this transport binding. - """ - - -class Config(BaseModel): - """ - Entity-specific configuration. Structure defined by each entity's schema. - """ - - model_config = ConfigDict( - extra="allow", - ) - delegate: list[str] | None = None - """ - Delegations the business allows. At service-level, declares available delegations. In UCP responses, confirms accepted delegations for this session. - """ - color_scheme: list[Literal["light", "dark"]] | None = None - """ - Color schemes the business supports. Hosts use ec_color_scheme query parameter to request a scheme from this list. - """ - - -class BusinessSchema7(BaseModel): - """ - Service binding for business/merchant configuration. May override platform endpoints. - """ - - model_config = ConfigDict( - extra="allow", - ) - version: str = Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$") - """ - Entity version in YYYY-MM-DD format. - """ - spec: AnyUrl | None = None - """ - URL to human-readable specification document. - """ - schema_: AnyUrl | None = Field(None, alias="schema") - """ - URL to JSON Schema defining this entity's structure and payloads. - """ - id: str | None = None - """ - Unique identifier for this entity instance. Used to disambiguate when multiple instances exist. - """ - config: Config | None = Field(None, title="Embedded Transport Config") - """ - Entity-specific configuration. Structure defined by each entity's schema. - """ - transport: Literal["embedded"] - """ - Transport protocol for this service binding. - """ - endpoint: AnyUrl | None = None - """ - Endpoint URL for this transport binding. - """ - - -BusinessSchema3 = TypeAliasType( - "BusinessSchema3", - Annotated[ - BusinessSchema | BusinessSchema5 | BusinessSchema6 | BusinessSchema7, - Field(..., title="Service (Business Schema)"), - ], +from .models import ( + ServiceBase, + ServiceBusinessSchema, + ServicePlatformSchema, + ServiceResponseSchema, ) -""" -Service binding for business/merchant configuration. May override platform endpoints. -""" - -class ResponseSchema(BaseModel): - """ - Service binding in API responses. Includes per-resource transport configuration via typed config. - """ - - model_config = ConfigDict( - extra="allow", - ) - version: str = Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$") - """ - Entity version in YYYY-MM-DD format. - """ - spec: AnyUrl | None = None - """ - URL to human-readable specification document. - """ - schema_: AnyUrl | None = Field(None, alias="schema") - """ - URL to JSON Schema defining this entity's structure and payloads. - """ - id: str | None = None - """ - Unique identifier for this entity instance. Used to disambiguate when multiple instances exist. - """ - config: dict[str, Any] | None = None - """ - Entity-specific configuration. Structure defined by each entity's schema. - """ - transport: Literal["rest"] - """ - Transport protocol for this service binding. - """ - endpoint: AnyUrl | None = None - """ - Endpoint URL for this transport binding. - """ - - -class ResponseSchema4(BaseModel): - """ - Service binding in API responses. Includes per-resource transport configuration via typed config. - """ - - model_config = ConfigDict( - extra="allow", - ) - version: str = Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$") - """ - Entity version in YYYY-MM-DD format. - """ - spec: AnyUrl | None = None - """ - URL to human-readable specification document. - """ - schema_: AnyUrl | None = Field(None, alias="schema") - """ - URL to JSON Schema defining this entity's structure and payloads. - """ - id: str | None = None - """ - Unique identifier for this entity instance. Used to disambiguate when multiple instances exist. - """ - config: dict[str, Any] | None = None - """ - Entity-specific configuration. Structure defined by each entity's schema. - """ - transport: Literal["mcp"] - """ - Transport protocol for this service binding. - """ - endpoint: AnyUrl | None = None - """ - Endpoint URL for this transport binding. - """ - - -class ResponseSchema5(BaseModel): - """ - Service binding in API responses. Includes per-resource transport configuration via typed config. - """ - - model_config = ConfigDict( - extra="allow", - ) - version: str = Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$") - """ - Entity version in YYYY-MM-DD format. - """ - spec: AnyUrl | None = None - """ - URL to human-readable specification document. - """ - schema_: AnyUrl | None = Field(None, alias="schema") - """ - URL to JSON Schema defining this entity's structure and payloads. - """ - id: str | None = None - """ - Unique identifier for this entity instance. Used to disambiguate when multiple instances exist. - """ - config: dict[str, Any] | None = None - """ - Entity-specific configuration. Structure defined by each entity's schema. - """ - transport: Literal["a2a"] - """ - Transport protocol for this service binding. - """ - endpoint: AnyUrl | None = None - """ - Endpoint URL for this transport binding. - """ - - -class ResponseSchema6(BaseModel): - """ - Service binding in API responses. Includes per-resource transport configuration via typed config. - """ - - model_config = ConfigDict( - extra="allow", - ) - version: str = Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$") - """ - Entity version in YYYY-MM-DD format. - """ - spec: AnyUrl | None = None - """ - URL to human-readable specification document. - """ - schema_: AnyUrl | None = Field(None, alias="schema") - """ - URL to JSON Schema defining this entity's structure and payloads. - """ - id: str | None = None - """ - Unique identifier for this entity instance. Used to disambiguate when multiple instances exist. - """ - config: Config | None = Field(None, title="Embedded Transport Config") - """ - Entity-specific configuration. Structure defined by each entity's schema. - """ - transport: Literal["embedded"] - """ - Transport protocol for this service binding. - """ - endpoint: AnyUrl | None = None - """ - Endpoint URL for this transport binding. - """ - - -ResponseSchema2 = TypeAliasType( - "ResponseSchema2", - Annotated[ - ResponseSchema | ResponseSchema4 | ResponseSchema5 | ResponseSchema6, - Field(..., title="Service (Response Schema)"), - ], -) -""" -Service binding in API responses. Includes per-resource transport configuration via typed config. -""" +Base = ServiceBase +__all__ = [ + "ServiceBase", + "ServiceBusinessSchema", + "ServicePlatformSchema", + "ServiceResponseSchema", + "Base", +] diff --git a/src/ucp_sdk/models/schemas/shopping/__init__.py b/src/ucp_sdk/models/schemas/shopping/__init__.py index 1252d6b..bccc166 100644 --- a/src/ucp_sdk/models/schemas/shopping/__init__.py +++ b/src/ucp_sdk/models/schemas/shopping/__init__.py @@ -1,17 +1,6 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +"""Shopping models.""" -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +from .checkout import * # noqa: F403 +from .cart import * # noqa: F403 +from .order import * # noqa: F403 +from .fulfillment import * # noqa: F403 diff --git a/src/ucp_sdk/models/schemas/shopping/buyer_consent.py b/src/ucp_sdk/models/schemas/shopping/buyer_consent.py deleted file mode 100644 index dbcb931..0000000 --- a/src/ucp_sdk/models/schemas/shopping/buyer_consent.py +++ /dev/null @@ -1,145 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated, Any, Literal - -from pydantic import BaseModel, ConfigDict, Field -from typing_extensions import TypeAliasType - -from ..common.types import link, reverse_domain_name -from .cart import Cart as Cart_1 -from .checkout import Checkout as Checkout_1 -from .types.buyer import Buyer as Buyer_1 - -BuyerConsentExtension = TypeAliasType( - "BuyerConsentExtension", - Annotated[Any, Field(..., title="Buyer Consent Extension")], -) -""" -Extends the buyer object with per-purpose consent. Each purpose is keyed by a reverse-DNS identifier and carries the current `granted` state, the `source` of that state (business default or platform-captured buyer decision), a `description`, optional `links`, and optional `segments` for finer-grained channel, vendor, or program decisions scoped to that purpose. -""" - - -class ConsentSegment(BaseModel): - """ - A buyer's consent decision for a specific refinement of a parent purpose (e.g., email marketing under the marketing purpose). Overrides the parent's `granted` value for this scope. Segments do not nest further. - """ - - model_config = ConfigDict( - extra="allow", - ) - granted: bool - """ - Whether consent has been granted for this segment. Overrides the parent purpose's `granted` value for this specific scope. - """ - source: Literal["business", "platform"] - """ - Identifies the party that asserted the current `granted` value for this segment. `business` means the value reflects the business's default policy; `platform` means the value reflects an explicit buyer decision captured by the platform. - """ - description: str - """ - Human-readable description of what the buyer is consenting to within this segment (e.g., 'Promotional emails and exclusive offers'). - """ - links: list[link.Link] | None = None - """ - Optional segment-specific links (e.g., channel terms or privacy disclosures). - """ - - -class ConsentPurpose(BaseModel): - """ - A buyer's consent decision for a purpose (e.g., marketing, analytics). Carries the current binary state, its source (business default or platform-captured buyer decision), human-readable context, and optional refinements scoping the decision to specific channels, vendors, or programs. - """ - - model_config = ConfigDict( - extra="allow", - ) - granted: bool - """ - Whether consent has been granted for this purpose. The `source` field identifies who asserted this state (business default or platform-captured buyer preference). - """ - source: Literal["business", "platform"] - """ - Identifies the party that asserted the current `granted` value. `business` means the value reflects the business's default policy; `platform` means the value reflects an explicit buyer decision captured by the platform. - """ - description: str - """ - Human-readable description of what the buyer is consenting to (e.g., 'Promotional communications across all channels'). - """ - links: list[link.Link] | None = None - """ - Optional links providing context (e.g., privacy policy, terms). - """ - segments: ( - dict[reverse_domain_name.ReverseDomainName, ConsentSegment] | None - ) = None - """ - Optional refinements scoping this purpose to specific channels, vendors, or programs. Keys are reverse-DNS identifiers. UCP currently defines two well-known segment identifiers under `dev.ucp.consent.marketing`: `dev.ucp.consent.marketing.email`, `dev.ucp.consent.marketing.sms`. Other segments follow vendor or merchant reverse-DNS conventions. - """ - - -Consent = TypeAliasType( - "Consent", dict[reverse_domain_name.ReverseDomainName, ConsentPurpose] -) -""" -Per-purpose consent. Keys are reverse-DNS purpose identifiers. UCP defines four well-known purposes: `dev.ucp.consent.marketing`, `dev.ucp.consent.analytics`, `dev.ucp.consent.preferences`, `dev.ucp.consent.sale_or_sharing`. Vendors and merchants may define additional purposes under their own reverse-DNS namespace. -""" - - -class Buyer(Buyer_1): - """ - Buyer object extended with per-purpose consent. - """ - - model_config = ConfigDict( - extra="allow", - ) - consent: Consent | None = None - """ - Per-purpose consent decisions and business-advertised consent options. - """ - - -class Cart(Cart_1): - """ - Cart extended with buyer consent. - """ - - model_config = ConfigDict( - extra="allow", - ) - buyer: Buyer | None = None - """ - Buyer with consent tracking. - """ - - -class Checkout(Checkout_1): - """ - Checkout extended with buyer consent. - """ - - model_config = ConfigDict( - extra="allow", - ) - buyer: Buyer | None = None - """ - Buyer with consent tracking. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/cart.py b/src/ucp_sdk/models/schemas/shopping/cart.py index 96b14aa..20540fe 100644 --- a/src/ucp_sdk/models/schemas/shopping/cart.py +++ b/src/ucp_sdk/models/schemas/shopping/cart.py @@ -1,107 +1,15 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Shopping cart models.""" from __future__ import annotations -from pydantic import AnyUrl, AwareDatetime, BaseModel, ConfigDict - -from .. import ucp as ucp_1 -from ..common.types import actions as actions_1 -from ..common.types import context as context_1 -from ..common.types import link, message, policy -from ..common.types import signals as signals_1 -from ..common.types import totals as totals_1 -from .checkout import Checkout as Checkout_1 -from .types import attribution as attribution_1 -from .types import buyer as buyer_1 -from .types import line_item - - -class Cart(BaseModel): - """ - Shopping cart with estimated pricing before checkout. Lightweight pre-purchase exploration with no payment info or complex status states. - """ - - model_config = ConfigDict( - extra="allow", - ) - ucp: ucp_1.UcpMetadata - id: str - """ - Unique cart identifier. - """ - line_items: list[line_item.LineItem] - """ - Cart line items. Same structure as checkout. Full replacement on update. - """ - context: context_1.Context | None = None - """ - Buyer signals for localization (country, region, postal_code). Merchant uses for pricing, availability, currency. Falls back to geo-IP if omitted. - """ - signals: signals_1.Signals | None = None - attribution: attribution_1.Attribution | None = None - buyer: buyer_1.Buyer | None = None - """ - Optional buyer information for personalized estimates. - """ - currency: str - """ - ISO 4217 currency code. Determined by merchant based on context or geo-IP. - """ - totals: totals_1.Totals - """ - Estimated cost breakdown. May be partial if shipping/tax not yet calculable. - """ - actions: actions_1.Actions | None = None - """ - Outstanding extension-defined Actions for this cart. - """ - messages: list[message.Message] | None = None - """ - Validation messages, warnings, or informational notices. - """ - links: list[link.Link] | None = None - """ - Optional merchant links (policies, FAQs). - """ - policies: list[policy.Policy] | None = None - """ - Policies (e.g., return/refund terms) that apply to the items in this cart. `applies_to` targets are relative to the response root; when absent or empty, refer to the URLs in `links[]`. - """ - continue_url: AnyUrl | None = None - """ - URL for cart handoff and session recovery. Enables sharing and human-in-the-loop flows. - """ - expires_at: AwareDatetime | None = None - """ - Cart expiry timestamp (RFC 3339). Optional. - """ - - -class Checkout(Checkout_1): - """ - Checkout extended with cart capability. Adds cart_id to create_checkout for cart-to-checkout conversion. - """ - - model_config = ConfigDict( - extra="allow", - ) - cart_id: str | None = None - """ - Cart ID to convert to checkout. Business MUST use cart contents (line_items, context, buyer) and MUST ignore overlapping fields in checkout payload. - """ +from ..models import ( + Cart, + CartCreateRequest, + CartUpdateRequest, +) + +__all__ = [ + "Cart", + "CartCreateRequest", + "CartUpdateRequest", +] diff --git a/src/ucp_sdk/models/schemas/shopping/cart_create_request.py b/src/ucp_sdk/models/schemas/shopping/cart_create_request.py deleted file mode 100644 index 42788fa..0000000 --- a/src/ucp_sdk/models/schemas/shopping/cart_create_request.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - -from ..common.types import context_create_request, signals_create_request -from .checkout_create_request import CheckoutCreateRequest -from .types import ( - attribution_create_request, - buyer_create_request, - line_item_create_request, -) - - -class CartCreateRequest(BaseModel): - """ - Shopping cart with estimated pricing before checkout. Lightweight pre-purchase exploration with no payment info or complex status states. - """ - - model_config = ConfigDict( - extra="allow", - ) - line_items: list[line_item_create_request.LineItemCreateRequest] - """ - Cart line items. Same structure as checkout. Full replacement on update. - """ - context: context_create_request.ContextCreateRequest | None = None - """ - Buyer signals for localization (country, region, postal_code). Merchant uses for pricing, availability, currency. Falls back to geo-IP if omitted. - """ - signals: signals_create_request.SignalsCreateRequest | None = None - attribution: attribution_create_request.AttributionCreateRequest | None = ( - None - ) - buyer: buyer_create_request.BuyerCreateRequest | None = None - """ - Optional buyer information for personalized estimates. - """ - - -class Checkout(CheckoutCreateRequest): - """ - Checkout extended with cart capability. Adds cart_id to create_checkout for cart-to-checkout conversion. - """ - - model_config = ConfigDict( - extra="allow", - ) - cart_id: str | None = None - """ - Cart ID to convert to checkout. Business MUST use cart contents (line_items, context, buyer) and MUST ignore overlapping fields in checkout payload. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/cart_update_request.py b/src/ucp_sdk/models/schemas/shopping/cart_update_request.py deleted file mode 100644 index 83c6418..0000000 --- a/src/ucp_sdk/models/schemas/shopping/cart_update_request.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - -from ..common.types import context_update_request, signals_update_request -from .checkout_update_request import CheckoutUpdateRequest -from .types import ( - attribution_update_request, - buyer_update_request, - line_item_update_request, -) - - -class CartUpdateRequest(BaseModel): - """ - Shopping cart with estimated pricing before checkout. Lightweight pre-purchase exploration with no payment info or complex status states. - """ - - model_config = ConfigDict( - extra="allow", - ) - line_items: list[line_item_update_request.LineItemUpdateRequest] - """ - Cart line items. Same structure as checkout. Full replacement on update. - """ - context: context_update_request.ContextUpdateRequest | None = None - """ - Buyer signals for localization (country, region, postal_code). Merchant uses for pricing, availability, currency. Falls back to geo-IP if omitted. - """ - signals: signals_update_request.SignalsUpdateRequest | None = None - attribution: attribution_update_request.AttributionUpdateRequest | None = ( - None - ) - buyer: buyer_update_request.BuyerUpdateRequest | None = None - """ - Optional buyer information for personalized estimates. - """ - - -class Checkout(CheckoutUpdateRequest): - """ - Checkout extended with cart capability. Adds cart_id to create_checkout for cart-to-checkout conversion. - """ - - model_config = ConfigDict( - extra="allow", - ) - cart_id: str | None = None - """ - Cart ID to convert to checkout. Business MUST use cart contents (line_items, context, buyer) and MUST ignore overlapping fields in checkout payload. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/catalog_lookup.py b/src/ucp_sdk/models/schemas/shopping/catalog_lookup.py deleted file mode 100644 index a6f02fc..0000000 --- a/src/ucp_sdk/models/schemas/shopping/catalog_lookup.py +++ /dev/null @@ -1,191 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field - -from .. import ucp as ucp_1 -from ..common.types import actions as actions_1 -from ..common.types import context as context_1 -from ..common.types import message, policy -from ..common.types import signals as signals_1 -from .types import attribution as attribution_1 -from .types import ( - detail_option_value, - input_correlation, - search_filters, - selected_option, -) -from .types.product import Product as Product_1 -from .types.variant import Variant - - -class CatalogLookup(BaseModel): - """ - Product/variant lookup by identifier. Supports batch retrieval (lookup_catalog) and single-product detail (get_product). - """ - - model_config = ConfigDict( - extra="allow", - ) - - -class LookupVariant(Variant): - """ - Variant with required correlation metadata for lookup responses. - """ - - model_config = ConfigDict( - extra="allow", - ) - inputs: list[input_correlation.InputCorrelation] = Field(..., min_length=1) - """ - Which request identifiers resolved to this variant, and how. Each entry maps a request ID to its match type. - """ - - -class LookupRequest(BaseModel): - """ - Request body for catalog lookup. - """ - - model_config = ConfigDict( - extra="allow", - ) - ids: list[str] = Field(..., min_length=1) - """ - Identifiers to lookup. Implementations MUST support product ID and variant ID; MAY support secondary identifiers (SKU, handle, etc.). - """ - filters: search_filters.SearchFilters | None = None - """ - Filter criteria to narrow returned products and variants. All specified filters combine with AND logic. - """ - context: context_1.Context | None = None - signals: signals_1.Signals | None = None - attribution: attribution_1.Attribution | None = None - - -class GetProductRequest(BaseModel): - """ - Request body for single-product retrieval. Supports interactive variant narrowing via selected and preferences. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Product or variant identifier. Implementations MUST support product ID and variant ID. - """ - selected: list[selected_option.SelectedOption] | None = None - """ - Partial or full option selections for interactive variant narrowing. When provided, response option values include availability signals (available, exists) relative to these selections. - """ - preferences: list[str] | None = None - """ - Option names in relaxation priority order. When no exact variant matches all selections, the server drops options from the end of this list first. E.g., ['Color', 'Size'] keeps Color and relaxes Size. - """ - filters: search_filters.SearchFilters | None = None - """ - Filter criteria to narrow returned variants. All specified filters combine with AND logic. - """ - context: context_1.Context | None = None - signals: signals_1.Signals | None = None - attribution: attribution_1.Attribution | None = None - - -class Option(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - name: str - values: list[detail_option_value.DetailOptionValue] = Field( - ..., min_length=1 - ) - - -class Product(Product_1): - model_config = ConfigDict( - extra="allow", - ) - variants: list[LookupVariant] | None = None - - -class LookupResponse(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - ucp: ucp_1.ResponseCatalogSchema - products: list[Product] - """ - Products matching the requested identifiers. May contain fewer items if some identifiers not found, or more if identifiers match multiple products. - """ - actions: actions_1.Actions | None = None - """ - Outstanding extension-defined Actions for this catalog lookup response. - """ - messages: list[message.Message] | None = None - """ - Errors, warnings, or informational messages about the requested items. - """ - policies: list[policy.Policy] | None = None - """ - Policies (e.g., return/refund terms) that apply to the products in this response. `applies_to` targets are relative to the response root; when absent or empty, refer to the URLs in `links[]`. - """ - - -class DetailProduct(Product_1): - """ - A product in a get_product response, extended with effective selections and availability signals on option values. - """ - - model_config = ConfigDict( - extra="allow", - ) - selected: list[selected_option.SelectedOption] | None = None - """ - Effective option selections that anchor the featured variant and availability signals. Required when the product has configurable options; may be empty or omitted for products with no option axes. - """ - options: list[Option] | None = None - """ - Product options with availability signals relative to the effective selections. - """ - - -class GetProductResponse(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - ucp: ucp_1.ResponseCatalogSchema - product: DetailProduct - """ - The requested product with full detail. Singular — this is a single-resource operation. - """ - actions: actions_1.Actions | None = None - """ - Outstanding extension-defined Actions for this product response. - """ - messages: list[message.Message] | None = None - """ - Warnings or informational messages about the product (e.g., price recently changed, limited availability). - """ - policies: list[policy.Policy] | None = None - """ - Policies (e.g., return/refund terms) that apply to this product. `applies_to` targets are relative to the response root; when absent or empty, refer to the URLs in `links[]`. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/catalog_search.py b/src/ucp_sdk/models/schemas/shopping/catalog_search.py deleted file mode 100644 index 6fef5d6..0000000 --- a/src/ucp_sdk/models/schemas/shopping/catalog_search.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - -from .. import ucp as ucp_1 -from ..common.types import actions as actions_1 -from ..common.types import context as context_1 -from ..common.types import message -from ..common.types import pagination as pagination_1 -from ..common.types import policy -from ..common.types import signals as signals_1 -from .types import attribution as attribution_1 -from .types import product, search_filters - - -class CatalogSearch(BaseModel): - """ - Product catalog search capability. - """ - - model_config = ConfigDict( - extra="allow", - ) - - -class SearchRequest(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - query: str | None = None - """ - Free-text search query. - """ - context: context_1.Context | None = None - signals: signals_1.Signals | None = None - attribution: attribution_1.Attribution | None = None - filters: search_filters.SearchFilters | None = None - pagination: pagination_1.Request | None = None - - -class SearchResponse(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - ucp: ucp_1.ResponseCatalogSchema - products: list[product.Product] - """ - Products matching the search criteria. - """ - pagination: pagination_1.Response | None = None - actions: actions_1.Actions | None = None - """ - Outstanding extension-defined Actions for this catalog search response. - """ - messages: list[message.Message] | None = None - """ - Errors, warnings, or informational messages about the search results. - """ - policies: list[policy.Policy] | None = None - """ - Policies (e.g., return/refund terms) that apply to the products in these search results. `applies_to` targets are relative to the response root; when absent or empty, refer to the URLs in `links[]`. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/checkout.py b/src/ucp_sdk/models/schemas/shopping/checkout.py index ede35c5..4ec318b 100644 --- a/src/ucp_sdk/models/schemas/shopping/checkout.py +++ b/src/ucp_sdk/models/schemas/shopping/checkout.py @@ -1,109 +1,17 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Shopping checkout models.""" from __future__ import annotations -from typing import Literal - -from pydantic import AnyUrl, AwareDatetime, BaseModel, ConfigDict - -from .. import ucp as ucp_1 -from ..common.types import actions as actions_1 -from ..common.types import context as context_1 -from ..common.types import link, message -from ..common.types import payment as payment_1 -from ..common.types import policy -from ..common.types import signals as signals_1 -from ..common.types import totals as totals_1 -from .types import attribution as attribution_1 -from .types import buyer as buyer_1 -from .types import line_item, order_confirmation - - -class Checkout(BaseModel): - """ - Base checkout schema. Extensions compose onto this using allOf. - """ - - model_config = ConfigDict( - extra="allow", - ) - ucp: ucp_1.UcpMetadata - id: str - """ - Unique identifier of the checkout session. - """ - line_items: list[line_item.LineItem] - """ - List of line items being checked out. - """ - buyer: buyer_1.Buyer | None = None - """ - Representation of the buyer. - """ - context: context_1.Context | None = None - signals: signals_1.Signals | None = None - attribution: attribution_1.Attribution | None = None - status: Literal[ - "incomplete", - "requires_escalation", - "ready_for_complete", - "complete_in_progress", - "completed", - "canceled", - ] - """ - Checkout state indicating the current phase and required processing. See Checkout Status lifecycle documentation for state transition details. - """ - currency: str - """ - ISO 4217 currency code reflecting the merchant's market determination. Derived from address, context, and geo IP—buyers provide signals, merchants determine currency. - """ - totals: totals_1.Totals - """ - Different cart totals. - """ - actions: actions_1.Actions | None = None - """ - Outstanding extension-defined Actions for this checkout. - """ - messages: list[message.Message] | None = None - """ - List of messages with error and info about the checkout session state. - """ - links: list[link.Link] - """ - Links to be displayed by the platform (Privacy Policy, TOS). Mandatory for legal compliance. - """ - policies: list[policy.Policy] | None = None - """ - Policies (e.g., return/refund terms) that apply to the items in this checkout. `applies_to` targets are relative to the response root; when absent or empty, refer to the URLs in `links[]`. - """ - expires_at: AwareDatetime | None = None - """ - RFC 3339 expiry timestamp. Default TTL is 6 hours from creation if not sent. - """ - continue_url: AnyUrl | None = None - """ - URL for checkout handoff and session recovery. MUST be provided when status is requires_escalation. See specification for format and availability requirements. - """ - payment: payment_1.Payment | None = None - order: order_confirmation.OrderConfirmation | None = None - """ - Details about an order created for this checkout session. - """ +from ..models import ( + Checkout, + CheckoutCompleteRequest, + CheckoutCreateRequest, + CheckoutUpdateRequest, +) + +__all__ = [ + "Checkout", + "CheckoutCompleteRequest", + "CheckoutCreateRequest", + "CheckoutUpdateRequest", +] diff --git a/src/ucp_sdk/models/schemas/shopping/checkout_complete_request.py b/src/ucp_sdk/models/schemas/shopping/checkout_complete_request.py deleted file mode 100644 index 94068b2..0000000 --- a/src/ucp_sdk/models/schemas/shopping/checkout_complete_request.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - -from ..common.types import payment_complete_request, signals_complete_request -from .types import attribution_complete_request - - -class CheckoutCompleteRequest(BaseModel): - """ - Base checkout schema. Extensions compose onto this using allOf. - """ - - model_config = ConfigDict( - extra="allow", - ) - signals: signals_complete_request.SignalsCompleteRequest | None = None - attribution: ( - attribution_complete_request.AttributionCompleteRequest | None - ) = None - payment: payment_complete_request.PaymentCompleteRequest diff --git a/src/ucp_sdk/models/schemas/shopping/checkout_create_request.py b/src/ucp_sdk/models/schemas/shopping/checkout_create_request.py deleted file mode 100644 index d09b1ae..0000000 --- a/src/ucp_sdk/models/schemas/shopping/checkout_create_request.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - -from ..common.types import ( - context_create_request, - payment_create_request, - signals_create_request, -) -from .types import ( - attribution_create_request, - buyer_create_request, - line_item_create_request, -) - - -class CheckoutCreateRequest(BaseModel): - """ - Base checkout schema. Extensions compose onto this using allOf. - """ - - model_config = ConfigDict( - extra="allow", - ) - line_items: list[line_item_create_request.LineItemCreateRequest] - """ - List of line items being checked out. - """ - buyer: buyer_create_request.BuyerCreateRequest | None = None - """ - Representation of the buyer. - """ - context: context_create_request.ContextCreateRequest | None = None - signals: signals_create_request.SignalsCreateRequest | None = None - attribution: attribution_create_request.AttributionCreateRequest | None = ( - None - ) - payment: payment_create_request.PaymentCreateRequest | None = None diff --git a/src/ucp_sdk/models/schemas/shopping/checkout_update_request.py b/src/ucp_sdk/models/schemas/shopping/checkout_update_request.py deleted file mode 100644 index f2eb2c2..0000000 --- a/src/ucp_sdk/models/schemas/shopping/checkout_update_request.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - -from ..common.types import ( - context_update_request, - payment_update_request, - signals_update_request, -) -from .types import ( - attribution_update_request, - buyer_update_request, - line_item_update_request, -) - - -class CheckoutUpdateRequest(BaseModel): - """ - Base checkout schema. Extensions compose onto this using allOf. - """ - - model_config = ConfigDict( - extra="allow", - ) - line_items: list[line_item_update_request.LineItemUpdateRequest] - """ - List of line items being checked out. - """ - buyer: buyer_update_request.BuyerUpdateRequest | None = None - """ - Representation of the buyer. - """ - context: context_update_request.ContextUpdateRequest | None = None - signals: signals_update_request.SignalsUpdateRequest | None = None - attribution: attribution_update_request.AttributionUpdateRequest | None = ( - None - ) - payment: payment_update_request.PaymentUpdateRequest | None = None diff --git a/src/ucp_sdk/models/schemas/shopping/discount.py b/src/ucp_sdk/models/schemas/shopping/discount.py deleted file mode 100644 index 33ecbdd..0000000 --- a/src/ucp_sdk/models/schemas/shopping/discount.py +++ /dev/null @@ -1,140 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated, Any, Literal - -from pydantic import BaseModel, ConfigDict, Field -from typing_extensions import TypeAliasType - -from ..common.types import amount as amount_1 -from ..common.types import reverse_domain_name -from .cart import Cart as Cart_1 -from .checkout import Checkout as Checkout_1 - -DiscountExtension = TypeAliasType( - "DiscountExtension", Annotated[Any, Field(..., title="Discount Extension")] -) -""" -Extends Cart and Checkout with discount support, including discount codes, automatic discounts, and eligibility-triggered provisional discounts. -""" - - -class Allocation(BaseModel): - """ - Breakdown of how a discount amount was allocated to a specific target. - """ - - model_config = ConfigDict( - extra="allow", - ) - path: str - """ - RFC 9535 JSONPath to the allocation target (e.g., '$.line_items[0]', '$.totals[?@.type == "fulfillment"]'). - """ - amount: amount_1.Amount - """ - Amount allocated to this target in ISO 4217 minor units. - """ - - -class AppliedDiscount(BaseModel): - """ - A discount that was successfully applied. - """ - - model_config = ConfigDict( - extra="allow", - ) - code: str | None = None - """ - The discount code. Omitted for automatic discounts. - """ - title: str - """ - Human-readable discount name (e.g., 'Summer Sale 20% Off'). - """ - amount: amount_1.Amount - """ - Total discount amount in ISO 4217 minor units. - """ - automatic: bool | None = False - """ - True if applied automatically by merchant rules (no code required). - """ - method: Literal["each", "across"] | None = None - """ - Allocation method. 'each' = applied independently per item. 'across' = split proportionally by value. - """ - priority: int | None = Field(None, ge=1) - """ - Stacking order for discount calculation. Lower numbers applied first (1 = first). - """ - provisional: bool | None = False - """ - True if this discount requires additional verification. - """ - eligibility: reverse_domain_name.ReverseDomainName | None = None - """ - The eligibility claim accepted by the Business for this discount. Corresponds to a value from context.eligibility. Omitted for code-based and non-eligibility automatic discounts. - """ - allocations: list[Allocation] | None = None - """ - Breakdown of where this discount was allocated. Sum of allocation amounts equals total amount. - """ - - -class DiscountsObject(BaseModel): - """ - Discount codes input and applied discounts output. - """ - - model_config = ConfigDict( - extra="allow", - ) - codes: list[str] | None = None - """ - Discount codes to apply. Case-insensitive. Replaces previously submitted codes. Send empty array to clear. - """ - applied: list[AppliedDiscount] | None = None - """ - Discounts successfully applied (code-based and automatic). - """ - - -class Cart(Cart_1): - """ - Cart extended with discount capability. - """ - - model_config = ConfigDict( - extra="allow", - ) - discounts: DiscountsObject | None = None - - -class Checkout(Checkout_1): - """ - Checkout extended with discount capability. - """ - - model_config = ConfigDict( - extra="allow", - ) - discounts: DiscountsObject | None = None diff --git a/src/ucp_sdk/models/schemas/shopping/fulfillment.py b/src/ucp_sdk/models/schemas/shopping/fulfillment.py index 24b867d..42bbdea 100644 --- a/src/ucp_sdk/models/schemas/shopping/fulfillment.py +++ b/src/ucp_sdk/models/schemas/shopping/fulfillment.py @@ -1,308 +1,15 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Shopping fulfillment models.""" from __future__ import annotations -from typing import Annotated, Any - -from pydantic import BaseModel, ConfigDict, Field -from typing_extensions import TypeAliasType - -from ..capability import BusinessSchema, PlatformSchema -from ..common.types import description as description_1 -from .catalog_lookup import DetailProduct -from .catalog_lookup import GetProductRequest as GetProductRequest_1 -from .catalog_lookup import GetProductResponse as GetProductResponse_1 -from .catalog_lookup import LookupRequest as LookupRequest_1 -from .catalog_lookup import LookupResponse as LookupResponse_1 -from .catalog_lookup import LookupVariant -from .catalog_search import SearchRequest as SearchRequest_1 -from .catalog_search import SearchResponse as SearchResponse_1 -from .checkout import Checkout as Checkout_1 -from .types import availability as availability_1 -from .types import business_fulfillment_config -from .types import fulfillment as fulfillment_1 -from .types import ( - fulfillment_available_method, - fulfillment_destination_filter, - fulfillment_group, - fulfillment_method, - fulfillment_option, - fulfillment_option_base, - platform_fulfillment_config, -) -from .types.product import Product -from .types.search_filters import SearchFilters -from .types.variant import Variant - -FulfillmentExtension = TypeAliasType( - "FulfillmentExtension", - Annotated[Any, Field(..., title="Fulfillment Extension")], +from ..models import ( + Fulfillment, + FulfillmentCreateRequest, + FulfillmentUpdateRequest, ) -""" -Extends Catalog with fulfillment discovery and Checkout with hierarchical fulfillment. -""" - - -FulfillmentAvailableMethod = TypeAliasType( - "FulfillmentAvailableMethod", - fulfillment_available_method.FulfillmentAvailableMethod, -) - - -class CatalogFulfillmentMethod(BaseModel): - """ - A fulfillment method on a catalog variant: how the variant can be fulfilled, and its availability. - """ - - model_config = ConfigDict( - extra="allow", - ) - type: str - """ - Fulfillment method type. Well-known values: `shipping`, `pickup`. Businesses MAY use additional values. - """ - description: description_1.Description | None = None - """ - Short buyer-facing summary (e.g. 'Ships in 2–4 business days'). - """ - availability: availability_1.Availability | None = None - """ - Availability of this variant via this method at the specified or inferred location. - """ - location: str | None = None - """ - Stable, opaque identifier for the Business Location resolved for this place-based fulfillment method. The Business recognizes the same ID when submitted as `selected_destination_id` for that method; recognition does not reserve inventory or guarantee eligibility, and current terms are revalidated. - """ - options: list[fulfillment_option_base.FulfillmentOptionBase] | None = None - """ - Representative fulfillment options for this method (e.g. Standard, Express). Without a destination or full cart, a Business SHOULD preview meaningful boundary options (e.g. cheapest, fastest); more specific options are negotiated in Checkout once line items and destination are known. - """ - - -class CatalogFulfillment(BaseModel): - """ - How a catalog variant can be fulfilled. Mirrors checkout `fulfillment`. - """ - - model_config = ConfigDict( - extra="allow", - ) - methods: list[CatalogFulfillmentMethod] | None = None - """ - Fulfillment methods for this variant. - """ - - -class FulfillmentPlatformSchema(PlatformSchema): - """ - Platform-level fulfillment capability configuration - """ - - model_config = ConfigDict( - extra="allow", - ) - config: platform_fulfillment_config.PlatformFulfillmentConfig | None = None - """ - Platform fulfillment configuration - """ - - -class FulfillmentBusinessSchema(BusinessSchema): - """ - Business-level fulfillment capability configuration - """ - - model_config = ConfigDict( - extra="allow", - ) - config: business_fulfillment_config.BusinessFulfillmentConfig | None = None - """ - Business fulfillment configuration - """ - - -FulfillmentOption = TypeAliasType( - "FulfillmentOption", fulfillment_option.FulfillmentOption -) - - -class FulfillmentVariant(Variant): - """ - A catalog variant with fulfillment. - """ - - model_config = ConfigDict( - extra="allow", - ) - fulfillment: CatalogFulfillment | None = None - - -class FulfillmentLookupVariant(LookupVariant): - """ - A lookup variant (carrying input correlation) enriched with fulfillment. - """ - - model_config = ConfigDict( - extra="allow", - ) - fulfillment: CatalogFulfillment | None = None - - -class FulfillmentSearchFilters(SearchFilters): - """ - Catalog filters extended with a fulfillment destination filter and a method-type filter. - """ - - model_config = ConfigDict( - extra="allow", - ) - fulfills_to: ( - fulfillment_destination_filter.FulfillmentDestinationFilter | None - ) = None - """ - Explicit destination where items are fulfilled. It may differ from the locality or Business Location supplied in `context` (e.g. a gift delivered directly to the recipient). The filter restricts results to what can be fulfilled there and seeds method `availability`. It supersedes `context` only for fulfillment destination and availability resolution. - """ - methods: list[str] | None = None - """ - Restrict results to these fulfillment method types (e.g. ["pickup"]). Well-known values: `shipping`, `pickup`. - """ - - -class FulfillmentSearchRequest(SearchRequest_1): - model_config = ConfigDict( - extra="allow", - ) - filters: FulfillmentSearchFilters | None = None - - -class FulfillmentLookupRequest(LookupRequest_1): - model_config = ConfigDict( - extra="allow", - ) - filters: FulfillmentSearchFilters | None = None - - -class FulfillmentGetProductRequest(GetProductRequest_1): - model_config = ConfigDict( - extra="allow", - ) - filters: FulfillmentSearchFilters | None = None - - -SearchRequest = TypeAliasType("SearchRequest", FulfillmentSearchRequest) - - -LookupRequest = TypeAliasType("LookupRequest", FulfillmentLookupRequest) - - -GetProductRequest = TypeAliasType( - "GetProductRequest", FulfillmentGetProductRequest -) - - -FulfillmentGroup = TypeAliasType( - "FulfillmentGroup", fulfillment_group.FulfillmentGroup -) - - -FulfillmentMethod = TypeAliasType( - "FulfillmentMethod", fulfillment_method.FulfillmentMethod -) - - -class FulfillmentProduct(Product): - """ - A catalog product whose variants are fulfillment-enriched. Used by search. - """ - - model_config = ConfigDict( - extra="allow", - ) - variants: list[FulfillmentVariant] | None = None - - -class FulfillmentLookupProduct(Product): - """ - A lookup product whose variants are fulfillment-enriched, preserving input correlation. Used by lookup. - """ - - model_config = ConfigDict( - extra="allow", - ) - variants: list[FulfillmentLookupVariant] | None = None - - -class FulfillmentDetailProduct(DetailProduct): - """ - A get_product detail product (carrying selected/options availability signals) whose variants are fulfillment-enriched. Used by get_product. - """ - - model_config = ConfigDict( - extra="allow", - ) - variants: list[FulfillmentVariant] | None = None - - -class FulfillmentSearchResponse(SearchResponse_1): - model_config = ConfigDict( - extra="allow", - ) - products: list[FulfillmentProduct] | None = None - - -class FulfillmentLookupResponse(LookupResponse_1): - model_config = ConfigDict( - extra="allow", - ) - products: list[FulfillmentLookupProduct] | None = None - - -class FulfillmentGetProductResponse(GetProductResponse_1): - model_config = ConfigDict( - extra="allow", - ) - product: FulfillmentDetailProduct | None = None - - -SearchResponse = TypeAliasType("SearchResponse", FulfillmentSearchResponse) - - -LookupResponse = TypeAliasType("LookupResponse", FulfillmentLookupResponse) - - -GetProductResponse = TypeAliasType( - "GetProductResponse", FulfillmentGetProductResponse -) - - -Fulfillment = TypeAliasType("Fulfillment", fulfillment_1.Fulfillment) - - -class Checkout(Checkout_1): - """ - Checkout extended with hierarchical fulfillment. - """ - model_config = ConfigDict( - extra="allow", - ) - fulfillment: Fulfillment | None = None - """ - Fulfillment details. - """ +__all__ = [ + "Fulfillment", + "FulfillmentCreateRequest", + "FulfillmentUpdateRequest", +] diff --git a/src/ucp_sdk/models/schemas/shopping/order.py b/src/ucp_sdk/models/schemas/shopping/order.py index 47e95f0..5aee0cd 100644 --- a/src/ucp_sdk/models/schemas/shopping/order.py +++ b/src/ucp_sdk/models/schemas/shopping/order.py @@ -1,119 +1,15 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Shopping order models.""" from __future__ import annotations -from pydantic import AnyUrl, BaseModel, ConfigDict - -from .. import ucp as ucp_1 -from ..common.types import message, policy -from ..common.types import totals as totals_1 -from .types import adjustment -from .types import attribution as attribution_1 -from .types import expectation, fulfillment_event, order_line_item - - -class PlatformSchema(BaseModel): - """ - Platform's order capability configuration. - """ - - model_config = ConfigDict( - extra="allow", - ) - webhook_url: AnyUrl - """ - URL where merchant sends order lifecycle events (webhooks). - """ - - -class Fulfillment(BaseModel): - """ - Fulfillment data: buyer expectations and what actually happened. - """ - - model_config = ConfigDict( - extra="allow", - ) - expectations: list[expectation.Expectation] | None = None - """ - Buyer-facing groups representing when/how items will be delivered. Can be split, merged, or adjusted post-order. - """ - events: list[fulfillment_event.FulfillmentEvent] | None = None - """ - Append-only event log of actual shipments. Each event references line items by ID. - """ - - -class Order(BaseModel): - """ - Order schema with line items, buyer-facing fulfillment expectations, and event logs. - """ - - model_config = ConfigDict( - extra="allow", - ) - ucp: ucp_1.UcpMetadata - id: str - """ - Unique order identifier. - """ - label: str | None = None - """ - Human-readable label for identifying the order. MUST only be provided by the business. - """ - checkout_id: str - """ - Associated checkout ID for reconciliation. - """ - permalink_url: AnyUrl - """ - Permalink to access the order on merchant site. - """ - line_items: list[order_line_item.OrderLineItem] - """ - Line items representing what was purchased — can change post-order via edits or exchanges. - """ - fulfillment: Fulfillment - """ - Fulfillment data: buyer expectations and what actually happened. - """ - adjustments: list[adjustment.Adjustment] | None = None - """ - Post-order events (refunds, returns, credits, disputes, cancellations, etc.) that exist independently of fulfillment. - """ - currency: str - """ - ISO 4217 currency code. MUST match the currency from the originating checkout session. - """ - totals: totals_1.Totals - """ - Different totals for the order. - """ - policies: list[policy.Policy] | None = None - """ - Snapshot of the policies that applied to the items at checkout, captured on the order as a durable record. `applies_to` targets are relative to the response root. - """ - messages: list[message.Message] | None = None - """ - Business outcome messages (errors, warnings, informational). Present when the business needs to communicate status or issues to the platform. - """ - attribution: attribution_1.Attribution | None = None - """ - Snapshot of the attribution associated with the originating checkout. Read-only on the order. - """ +from ..models import ( + Order, + OrderCreateRequest, + OrderUpdateRequest, +) + +__all__ = [ + "Order", + "OrderCreateRequest", + "OrderUpdateRequest", +] diff --git a/src/ucp_sdk/models/schemas/shopping/order_create_request.py b/src/ucp_sdk/models/schemas/shopping/order_create_request.py deleted file mode 100644 index e5f36ca..0000000 --- a/src/ucp_sdk/models/schemas/shopping/order_create_request.py +++ /dev/null @@ -1,116 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import AnyUrl, BaseModel, ConfigDict - -from .. import ucp_create_request -from ..common.types import message_create_request, totals_create_request -from .types import ( - adjustment_create_request, - expectation_create_request, - fulfillment_event_create_request, - order_line_item_create_request, -) - - -class PlatformSchema(BaseModel): - """ - Platform's order capability configuration. - """ - - model_config = ConfigDict( - extra="allow", - ) - webhook_url: AnyUrl - """ - URL where merchant sends order lifecycle events (webhooks). - """ - - -class Fulfillment(BaseModel): - """ - Fulfillment data: buyer expectations and what actually happened. - """ - - model_config = ConfigDict( - extra="allow", - ) - expectations: ( - list[expectation_create_request.ExpectationCreateRequest] | None - ) = None - """ - Buyer-facing groups representing when/how items will be delivered. Can be split, merged, or adjusted post-order. - """ - events: ( - list[fulfillment_event_create_request.FulfillmentEventCreateRequest] - | None - ) = None - """ - Append-only event log of actual shipments. Each event references line items by ID. - """ - - -class OrderCreateRequest(BaseModel): - """ - Order schema with line items, buyer-facing fulfillment expectations, and event logs. - """ - - model_config = ConfigDict( - extra="allow", - ) - ucp: ucp_create_request.UcpMetadataCreateRequest - id: str - """ - Unique order identifier. - """ - label: str | None = None - """ - Human-readable label for identifying the order. MUST only be provided by the business. - """ - checkout_id: str - """ - Associated checkout ID for reconciliation. - """ - permalink_url: AnyUrl - """ - Permalink to access the order on merchant site. - """ - line_items: list[order_line_item_create_request.OrderLineItemCreateRequest] - """ - Line items representing what was purchased — can change post-order via edits or exchanges. - """ - fulfillment: Fulfillment - """ - Fulfillment data: buyer expectations and what actually happened. - """ - adjustments: ( - list[adjustment_create_request.AdjustmentCreateRequest] | None - ) = None - """ - Post-order events (refunds, returns, credits, disputes, cancellations, etc.) that exist independently of fulfillment. - """ - totals: totals_create_request.TotalsCreateRequest - """ - Different totals for the order. - """ - messages: list[message_create_request.MessageCreateRequest] | None = None - """ - Business outcome messages (errors, warnings, informational). Present when the business needs to communicate status or issues to the platform. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/order_update_request.py b/src/ucp_sdk/models/schemas/shopping/order_update_request.py deleted file mode 100644 index 959b3ae..0000000 --- a/src/ucp_sdk/models/schemas/shopping/order_update_request.py +++ /dev/null @@ -1,116 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import AnyUrl, BaseModel, ConfigDict - -from .. import ucp_update_request -from ..common.types import message_update_request, totals_update_request -from .types import ( - adjustment_update_request, - expectation_update_request, - fulfillment_event_update_request, - order_line_item_update_request, -) - - -class PlatformSchema(BaseModel): - """ - Platform's order capability configuration. - """ - - model_config = ConfigDict( - extra="allow", - ) - webhook_url: AnyUrl - """ - URL where merchant sends order lifecycle events (webhooks). - """ - - -class Fulfillment(BaseModel): - """ - Fulfillment data: buyer expectations and what actually happened. - """ - - model_config = ConfigDict( - extra="allow", - ) - expectations: ( - list[expectation_update_request.ExpectationUpdateRequest] | None - ) = None - """ - Buyer-facing groups representing when/how items will be delivered. Can be split, merged, or adjusted post-order. - """ - events: ( - list[fulfillment_event_update_request.FulfillmentEventUpdateRequest] - | None - ) = None - """ - Append-only event log of actual shipments. Each event references line items by ID. - """ - - -class OrderUpdateRequest(BaseModel): - """ - Order schema with line items, buyer-facing fulfillment expectations, and event logs. - """ - - model_config = ConfigDict( - extra="allow", - ) - ucp: ucp_update_request.UcpMetadataUpdateRequest - id: str - """ - Unique order identifier. - """ - label: str | None = None - """ - Human-readable label for identifying the order. MUST only be provided by the business. - """ - checkout_id: str - """ - Associated checkout ID for reconciliation. - """ - permalink_url: AnyUrl - """ - Permalink to access the order on merchant site. - """ - line_items: list[order_line_item_update_request.OrderLineItemUpdateRequest] - """ - Line items representing what was purchased — can change post-order via edits or exchanges. - """ - fulfillment: Fulfillment - """ - Fulfillment data: buyer expectations and what actually happened. - """ - adjustments: ( - list[adjustment_update_request.AdjustmentUpdateRequest] | None - ) = None - """ - Post-order events (refunds, returns, credits, disputes, cancellations, etc.) that exist independently of fulfillment. - """ - totals: totals_update_request.TotalsUpdateRequest - """ - Different totals for the order. - """ - messages: list[message_update_request.MessageUpdateRequest] | None = None - """ - Business outcome messages (errors, warnings, informational). Present when the business needs to communicate status or issues to the platform. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/permalink.py b/src/ucp_sdk/models/schemas/shopping/permalink.py deleted file mode 100644 index b05c9a2..0000000 --- a/src/ucp_sdk/models/schemas/shopping/permalink.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated, Any - -from pydantic import AnyUrl, BaseModel, ConfigDict, Field -from typing_extensions import TypeAliasType - -PermalinkCapability = TypeAliasType( - "PermalinkCapability", - Annotated[Any, Field(..., title="Permalink Capability")], -) -""" -Browser-addressable shopping intent capability: defines a Business browser endpoint and redirect resolution. It defines no shopping-state fields of its own; permalink query parameters address existing UCP field paths. -""" - - -Endpoint = TypeAliasType("Endpoint", AnyUrl) -""" -Absolute HTTPS browser endpoint with a non-empty authority and without userinfo, query, fragment, whitespace, backslashes, or trailing slash. Optional compact item path and query parameters are appended to this endpoint. -""" - - -class Config(BaseModel): - """ - Business browser endpoint configuration for shopping permalinks. - """ - - model_config = ConfigDict( - extra="allow", - ) - endpoint: Endpoint - - -Permalink = TypeAliasType("Permalink", Any) diff --git a/src/ucp_sdk/models/schemas/shopping/types/__init__.py b/src/ucp_sdk/models/schemas/shopping/types/__init__.py index 1252d6b..921da05 100644 --- a/src/ucp_sdk/models/schemas/shopping/types/__init__.py +++ b/src/ucp_sdk/models/schemas/shopping/types/__init__.py @@ -1,17 +1,14 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +"""Shopping auxiliary types.""" -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +from .fulfillment_destination import * # noqa: F403 +from .fulfillment_method import * # noqa: F403 +from .business_fulfillment_config import * # noqa: F403 +from .line_item import * # noqa: F403 +from .item import * # noqa: F403 +from .buyer import * # noqa: F403 +from .attribution import * # noqa: F403 +from .order_line_item import * # noqa: F403 +from .product import * # noqa: F403 +from .variant import * # noqa: F403 +from .option_value import * # noqa: F403 +from .location_summary import * # noqa: F403 diff --git a/src/ucp_sdk/models/schemas/shopping/types/adjustment.py b/src/ucp_sdk/models/schemas/shopping/types/adjustment.py deleted file mode 100644 index 3ae0b00..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/adjustment.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Literal - -from pydantic import AwareDatetime, BaseModel, ConfigDict, Field - -from ...common.types import measure as measure_1 -from ...common.types import total - - -class LineItem(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Line item ID reference. - """ - quantity: int = Field(..., ge=-9007199254740991, le=9007199254740991) - """ - Signed integer count of steps of the referenced line item's `quantity_unit` (`10^-scale` × `unit`); when `quantity_unit` is absent, it counts whole items (`each`). Negative values represent reductions (e.g. returns); positive values represent additions (e.g. exchanges). - """ - measure: measure_1.Measure | None = None - """ - The settled measurement this adjustment reconciles (for example, actual picked weight), present when the line's price settles by measurement. Its unit identity MUST match the line's pricing basis (`item.unit_price` measure/reference unit); no unit conversion. A pure price settlement uses `quantity: 0` together with `measure` and a totals delta. - """ - - -class Adjustment(BaseModel): - """ - Post-order event that exists independently of fulfillment. Typically represents money movements but can be any post-order change. Polymorphic type that can optionally reference line items. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Adjustment event identifier. - """ - type: str - """ - Type of adjustment (open string). Typically money-related like: refund, return, credit, price_adjustment, dispute, cancellation. Can be any value that makes sense for the merchant's business. - """ - occurred_at: AwareDatetime - """ - RFC 3339 timestamp when this adjustment occurred. - """ - status: Literal["pending", "completed", "failed"] - """ - Adjustment status. - """ - line_items: list[LineItem] | None = None - """ - Which line items and quantities are affected (optional). - """ - totals: list[total.Total] | None = None - """ - Adjustment totals breakdown. Signed values - negative for money returned to buyer (refunds, credits), positive for additional charges (exchanges). - """ - description: str | None = None - """ - Human-readable reason or description (e.g., 'Defective item', 'Customer requested'). - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/adjustment_create_request.py b/src/ucp_sdk/models/schemas/shopping/types/adjustment_create_request.py deleted file mode 100644 index f3a7abb..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/adjustment_create_request.py +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Literal - -from pydantic import AwareDatetime, BaseModel, ConfigDict, Field - -from ...common.types import measure_create_request, total_create_request - - -class LineItem(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Line item ID reference. - """ - quantity: int = Field(..., ge=-9007199254740991, le=9007199254740991) - """ - Signed integer count of steps of the referenced line item's `quantity_unit` (`10^-scale` × `unit`); when `quantity_unit` is absent, it counts whole items (`each`). Negative values represent reductions (e.g. returns); positive values represent additions (e.g. exchanges). - """ - measure: measure_create_request.MeasureCreateRequest | None = None - """ - The settled measurement this adjustment reconciles (for example, actual picked weight), present when the line's price settles by measurement. Its unit identity MUST match the line's pricing basis (`item.unit_price` measure/reference unit); no unit conversion. A pure price settlement uses `quantity: 0` together with `measure` and a totals delta. - """ - - -class AdjustmentCreateRequest(BaseModel): - """ - Post-order event that exists independently of fulfillment. Typically represents money movements but can be any post-order change. Polymorphic type that can optionally reference line items. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Adjustment event identifier. - """ - type: str - """ - Type of adjustment (open string). Typically money-related like: refund, return, credit, price_adjustment, dispute, cancellation. Can be any value that makes sense for the merchant's business. - """ - occurred_at: AwareDatetime - """ - RFC 3339 timestamp when this adjustment occurred. - """ - status: Literal["pending", "completed", "failed"] - """ - Adjustment status. - """ - line_items: list[LineItem] | None = None - """ - Which line items and quantities are affected (optional). - """ - totals: list[total_create_request.TotalCreateRequest] | None = None - """ - Adjustment totals breakdown. Signed values - negative for money returned to buyer (refunds, credits), positive for additional charges (exchanges). - """ - description: str | None = None - """ - Human-readable reason or description (e.g., 'Defective item', 'Customer requested'). - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/adjustment_update_request.py b/src/ucp_sdk/models/schemas/shopping/types/adjustment_update_request.py deleted file mode 100644 index 3fd1b3b..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/adjustment_update_request.py +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Literal - -from pydantic import AwareDatetime, BaseModel, ConfigDict, Field - -from ...common.types import measure_update_request, total_update_request - - -class LineItem(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Line item ID reference. - """ - quantity: int = Field(..., ge=-9007199254740991, le=9007199254740991) - """ - Signed integer count of steps of the referenced line item's `quantity_unit` (`10^-scale` × `unit`); when `quantity_unit` is absent, it counts whole items (`each`). Negative values represent reductions (e.g. returns); positive values represent additions (e.g. exchanges). - """ - measure: measure_update_request.MeasureUpdateRequest | None = None - """ - The settled measurement this adjustment reconciles (for example, actual picked weight), present when the line's price settles by measurement. Its unit identity MUST match the line's pricing basis (`item.unit_price` measure/reference unit); no unit conversion. A pure price settlement uses `quantity: 0` together with `measure` and a totals delta. - """ - - -class AdjustmentUpdateRequest(BaseModel): - """ - Post-order event that exists independently of fulfillment. Typically represents money movements but can be any post-order change. Polymorphic type that can optionally reference line items. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Adjustment event identifier. - """ - type: str - """ - Type of adjustment (open string). Typically money-related like: refund, return, credit, price_adjustment, dispute, cancellation. Can be any value that makes sense for the merchant's business. - """ - occurred_at: AwareDatetime - """ - RFC 3339 timestamp when this adjustment occurred. - """ - status: Literal["pending", "completed", "failed"] - """ - Adjustment status. - """ - line_items: list[LineItem] | None = None - """ - Which line items and quantities are affected (optional). - """ - totals: list[total_update_request.TotalUpdateRequest] | None = None - """ - Adjustment totals breakdown. Signed values - negative for money returned to buyer (refunds, credits), positive for additional charges (exchanges). - """ - description: str | None = None - """ - Human-readable reason or description (e.g., 'Defective item', 'Customer requested'). - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/attribution.py b/src/ucp_sdk/models/schemas/shopping/types/attribution.py index 82c0051..3822b95 100644 --- a/src/ucp_sdk/models/schemas/shopping/types/attribution.py +++ b/src/ucp_sdk/models/schemas/shopping/types/attribution.py @@ -1,26 +1,7 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Attribution models.""" from __future__ import annotations -from typing_extensions import TypeAliasType +from ...models import Attribution -Attribution = TypeAliasType("Attribution", dict[str, str]) -""" -Platform-emitted referral and conversion-event context — campaign identifiers, click IDs, source/medium markers, etc. The same parameters platforms communicate via URL query parameters in browser-based flows. -""" +__all__ = ["Attribution"] diff --git a/src/ucp_sdk/models/schemas/shopping/types/attribution_complete_request.py b/src/ucp_sdk/models/schemas/shopping/types/attribution_complete_request.py deleted file mode 100644 index 48d4694..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/attribution_complete_request.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing_extensions import TypeAliasType - -AttributionCompleteRequest = TypeAliasType( - "AttributionCompleteRequest", dict[str, str] -) -""" -Platform-emitted referral and conversion-event context — campaign identifiers, click IDs, source/medium markers, etc. The same parameters platforms communicate via URL query parameters in browser-based flows. -""" diff --git a/src/ucp_sdk/models/schemas/shopping/types/attribution_create_request.py b/src/ucp_sdk/models/schemas/shopping/types/attribution_create_request.py deleted file mode 100644 index 0c4b537..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/attribution_create_request.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing_extensions import TypeAliasType - -AttributionCreateRequest = TypeAliasType( - "AttributionCreateRequest", dict[str, str] -) -""" -Platform-emitted referral and conversion-event context — campaign identifiers, click IDs, source/medium markers, etc. The same parameters platforms communicate via URL query parameters in browser-based flows. -""" diff --git a/src/ucp_sdk/models/schemas/shopping/types/attribution_update_request.py b/src/ucp_sdk/models/schemas/shopping/types/attribution_update_request.py deleted file mode 100644 index d19b78b..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/attribution_update_request.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing_extensions import TypeAliasType - -AttributionUpdateRequest = TypeAliasType( - "AttributionUpdateRequest", dict[str, str] -) -""" -Platform-emitted referral and conversion-event context — campaign identifiers, click IDs, source/medium markers, etc. The same parameters platforms communicate via URL query parameters in browser-based flows. -""" diff --git a/src/ucp_sdk/models/schemas/shopping/types/availability.py b/src/ucp_sdk/models/schemas/shopping/types/availability.py deleted file mode 100644 index 46b9ab9..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/availability.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class Availability(BaseModel): - """ - Availability of an item: whether it can be obtained, and a qualifying status. - """ - - model_config = ConfigDict( - extra="allow", - ) - available: bool | None = None - """ - Whether this can be obtained. See status for fulfillment details. - """ - status: str | None = None - """ - Qualifies available with fulfillment state. Well-known values: `in_stock`, `backorder`, `preorder`, `out_of_stock`, `discontinued`. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/business_fulfillment_config.py b/src/ucp_sdk/models/schemas/shopping/types/business_fulfillment_config.py index 7515f7e..6cee415 100644 --- a/src/ucp_sdk/models/schemas/shopping/types/business_fulfillment_config.py +++ b/src/ucp_sdk/models/schemas/shopping/types/business_fulfillment_config.py @@ -1,49 +1,7 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Business fulfillment configuration.""" from __future__ import annotations -from pydantic import BaseModel, ConfigDict - - -class MultiDestinationItem(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - method: str - """ - Fulfillment method type (e.g. `shipping`, `pickup`). Optional per-method constraints MAY be added alongside. - """ - - -class BusinessFulfillmentConfig(BaseModel): - """ - Business's fulfillment configuration. - """ +from ...models import BusinessFulfillmentConfig - model_config = ConfigDict( - extra="allow", - ) - multi_destination: list[MultiDestinationItem] | None = None - """ - Method types that permit multiple destinations within one cart (e.g. split shipping across addresses). Listing a method permits it; an omitted method does not. Open — businesses MAY list any method type. - """ - method_combinations: list[list[str]] | None = None - """ - Method-type combinations the business permits within one cart. Each inner array is a permitted set of method `type` values (e.g. shipping + pickup). - """ +__all__ = ["BusinessFulfillmentConfig"] diff --git a/src/ucp_sdk/models/schemas/shopping/types/buyer.py b/src/ucp_sdk/models/schemas/shopping/types/buyer.py index ff8ef6a..ce77527 100644 --- a/src/ucp_sdk/models/schemas/shopping/types/buyer.py +++ b/src/ucp_sdk/models/schemas/shopping/types/buyer.py @@ -1,43 +1,7 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Buyer models.""" from __future__ import annotations -from pydantic import BaseModel, ConfigDict - +from ...models import Buyer -class Buyer(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - first_name: str | None = None - """ - First name of the buyer. - """ - last_name: str | None = None - """ - Last name of the buyer. - """ - email: str | None = None - """ - Email of the buyer. - """ - phone_number: str | None = None - """ - E.164 standard. - """ +__all__ = ["Buyer"] diff --git a/src/ucp_sdk/models/schemas/shopping/types/buyer_create_request.py b/src/ucp_sdk/models/schemas/shopping/types/buyer_create_request.py deleted file mode 100644 index 0614375..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/buyer_create_request.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class BuyerCreateRequest(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - first_name: str | None = None - """ - First name of the buyer. - """ - last_name: str | None = None - """ - Last name of the buyer. - """ - email: str | None = None - """ - Email of the buyer. - """ - phone_number: str | None = None - """ - E.164 standard. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/buyer_update_request.py b/src/ucp_sdk/models/schemas/shopping/types/buyer_update_request.py deleted file mode 100644 index 5aea45e..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/buyer_update_request.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class BuyerUpdateRequest(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - first_name: str | None = None - """ - First name of the buyer. - """ - last_name: str | None = None - """ - Last name of the buyer. - """ - email: str | None = None - """ - Email of the buyer. - """ - phone_number: str | None = None - """ - E.164 standard. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/category.py b/src/ucp_sdk/models/schemas/shopping/types/category.py deleted file mode 100644 index 128be48..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/category.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class Category(BaseModel): - """ - A product category with optional taxonomy identifier. - """ - - model_config = ConfigDict( - extra="allow", - ) - value: str - """ - Category value or path (e.g., 'Apparel > Shirts', '1604'). - """ - taxonomy: str | None = None - """ - Source taxonomy. Well-known values: `google_product_category`, `shopify`, `merchant`. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/detail_option_value.py b/src/ucp_sdk/models/schemas/shopping/types/detail_option_value.py deleted file mode 100644 index 05e4dd6..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/detail_option_value.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import ConfigDict - -from .option_value import OptionValue - - -class DetailOptionValue(OptionValue): - """ - An option value with availability signals relative to the current selections. Used in get_product responses where selected context exists. - """ - - model_config = ConfigDict( - extra="allow", - ) - available: bool | None = None - """ - Whether a variant matching this value and the current option selections is purchasable. - """ - exists: bool | None = None - """ - Whether a variant matching this value and the current option selections exists in the catalog. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/expectation.py b/src/ucp_sdk/models/schemas/shopping/types/expectation.py deleted file mode 100644 index 59163e2..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/expectation.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field - -from ...common.types import postal_address - - -class LineItem(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Line item ID reference. - """ - quantity: int = Field(..., ge=1, le=9007199254740991) - """ - Integer count of steps of the referenced line item's `quantity_unit` (`10^-scale` × `unit`); when `quantity_unit` is absent, it counts whole items (`each`). - """ - - -class Expectation(BaseModel): - """ - Buyer-facing fulfillment expectation representing logical groupings of items (e.g., 'package'). Can be split, merged, or adjusted post-order to set buyer expectations for when/how items arrive. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Expectation identifier. - """ - line_items: list[LineItem] - """ - Which line items and quantities are in this expectation. - """ - method_type: str - """ - Delivery method type. Well-known values: `shipping`, `pickup`, `digital`; additional values MAY be used. - """ - destination: postal_address.PostalAddress - """ - Delivery destination address. - """ - description: str | None = None - """ - Human-readable delivery description (e.g., 'Arrives in 5-8 business days'). - """ - fulfillable_on: str | None = None - """ - When this expectation can be fulfilled: 'now' or ISO 8601 timestamp for future date (backorder, pre-order). - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/expectation_create_request.py b/src/ucp_sdk/models/schemas/shopping/types/expectation_create_request.py deleted file mode 100644 index 55f491f..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/expectation_create_request.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field - -from ...common.types import postal_address_create_request - - -class LineItem(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Line item ID reference. - """ - quantity: int = Field(..., ge=1, le=9007199254740991) - """ - Integer count of steps of the referenced line item's `quantity_unit` (`10^-scale` × `unit`); when `quantity_unit` is absent, it counts whole items (`each`). - """ - - -class ExpectationCreateRequest(BaseModel): - """ - Buyer-facing fulfillment expectation representing logical groupings of items (e.g., 'package'). Can be split, merged, or adjusted post-order to set buyer expectations for when/how items arrive. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Expectation identifier. - """ - line_items: list[LineItem] - """ - Which line items and quantities are in this expectation. - """ - method_type: str - """ - Delivery method type. Well-known values: `shipping`, `pickup`, `digital`; additional values MAY be used. - """ - destination: postal_address_create_request.PostalAddressCreateRequest - """ - Delivery destination address. - """ - description: str | None = None - """ - Human-readable delivery description (e.g., 'Arrives in 5-8 business days'). - """ - fulfillable_on: str | None = None - """ - When this expectation can be fulfilled: 'now' or ISO 8601 timestamp for future date (backorder, pre-order). - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/expectation_update_request.py b/src/ucp_sdk/models/schemas/shopping/types/expectation_update_request.py deleted file mode 100644 index a49a2d8..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/expectation_update_request.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field - -from ...common.types import postal_address_update_request - - -class LineItem(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Line item ID reference. - """ - quantity: int = Field(..., ge=1, le=9007199254740991) - """ - Integer count of steps of the referenced line item's `quantity_unit` (`10^-scale` × `unit`); when `quantity_unit` is absent, it counts whole items (`each`). - """ - - -class ExpectationUpdateRequest(BaseModel): - """ - Buyer-facing fulfillment expectation representing logical groupings of items (e.g., 'package'). Can be split, merged, or adjusted post-order to set buyer expectations for when/how items arrive. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Expectation identifier. - """ - line_items: list[LineItem] - """ - Which line items and quantities are in this expectation. - """ - method_type: str - """ - Delivery method type. Well-known values: `shipping`, `pickup`, `digital`; additional values MAY be used. - """ - destination: postal_address_update_request.PostalAddressUpdateRequest - """ - Delivery destination address. - """ - description: str | None = None - """ - Human-readable delivery description (e.g., 'Arrives in 5-8 business days'). - """ - fulfillable_on: str | None = None - """ - When this expectation can be fulfilled: 'now' or ISO 8601 timestamp for future date (backorder, pre-order). - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment.py deleted file mode 100644 index 8ff2c27..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/fulfillment.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - -from . import fulfillment_available_method, fulfillment_method - - -class Fulfillment(BaseModel): - """ - Container for fulfillment methods and availability. - """ - - model_config = ConfigDict( - extra="allow", - ) - methods: list[fulfillment_method.FulfillmentMethod] | None = None - """ - Fulfillment methods for cart items. - """ - available_methods: ( - list[fulfillment_available_method.FulfillmentAvailableMethod] | None - ) = None - """ - Inventory availability hints. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_available_method.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_available_method.py deleted file mode 100644 index d5df375..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_available_method.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class FulfillmentAvailableMethod(BaseModel): - """ - Inventory availability hint for a fulfillment method type. - """ - - model_config = ConfigDict( - extra="allow", - ) - type: str - """ - Fulfillment method type this availability applies to. Well-known values: `shipping`, `pickup`; businesses MAY use additional values. - """ - line_item_ids: list[str] - """ - Line items available for this fulfillment method. - """ - fulfillable_on: str | None = None - """ - 'now' for immediate availability, or ISO 8601 date for future (preorders, transfers). - """ - description: str | None = None - """ - Human-readable availability info (e.g., 'Available for pickup at Downtown Store today'). - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_available_method_create_request.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_available_method_create_request.py deleted file mode 100644 index 54854b7..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_available_method_create_request.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class FulfillmentAvailableMethodCreateRequest(BaseModel): - """ - Inventory availability hint for a fulfillment method type. - """ - - model_config = ConfigDict( - extra="allow", - ) diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_available_method_update_request.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_available_method_update_request.py deleted file mode 100644 index 678c052..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_available_method_update_request.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class FulfillmentAvailableMethodUpdateRequest(BaseModel): - """ - Inventory availability hint for a fulfillment method type. - """ - - model_config = ConfigDict( - extra="allow", - ) diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_create_request.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_create_request.py deleted file mode 100644 index 32accc9..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_create_request.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - -from . import fulfillment_method_create_request - - -class FulfillmentCreateRequest(BaseModel): - """ - Container for fulfillment methods and availability. - """ - - model_config = ConfigDict( - extra="allow", - ) - methods: ( - list[fulfillment_method_create_request.FulfillmentMethodCreateRequest] - | None - ) = None - """ - Fulfillment methods for cart items. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_destination.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_destination.py index fa07edf..af105ea 100644 --- a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_destination.py +++ b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_destination.py @@ -1,39 +1,23 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Fulfillment destination models.""" from __future__ import annotations -from pydantic import BaseModel, ConfigDict - - -class FulfillmentDestination(BaseModel): - """ - A destination for fulfillment. - """ +from ...models import ( + FulfillmentDestination, + LocationDestination, + LocationDestinationCreateRequest, + LocationDestinationUpdateRequest, + ShippingDestination, + ShippingDestinationCreateRequest, + ShippingDestinationUpdateRequest, +) - model_config = ConfigDict( - extra="allow", - ) - type: str - """ - Destination contract discriminator. Required in Business responses and optional in Platform requests. Well-known values: `shipping_address`, `business_location`. The enclosing method contract defines request defaults and which fields the Platform may write; negotiated extensions define additional values. - """ - id: str - """ - Fulfillment destination identifier. - """ +__all__ = [ + "FulfillmentDestination", + "LocationDestination", + "LocationDestinationCreateRequest", + "LocationDestinationUpdateRequest", + "ShippingDestination", + "ShippingDestinationCreateRequest", + "ShippingDestinationUpdateRequest", +] diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_destination_create_request.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_destination_create_request.py deleted file mode 100644 index d46a2e8..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_destination_create_request.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class FulfillmentDestinationCreateRequest(BaseModel): - """ - A destination for fulfillment. - """ - - model_config = ConfigDict( - extra="allow", - ) - type: str | None = None - """ - Destination contract discriminator. Required in Business responses and optional in Platform requests. Well-known values: `shipping_address`, `business_location`. The enclosing method contract defines request defaults and which fields the Platform may write; negotiated extensions define additional values. - """ - id: str | None = None - """ - Fulfillment destination identifier. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_destination_filter.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_destination_filter.py deleted file mode 100644 index b2af2b1..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_destination_filter.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import ConfigDict - -from ...common.types.locality import Locality - - -class FulfillmentDestinationFilter(Locality): - """ - A specific destination, named by value or by reference: a coarse locality (`address_country` / `address_region` / `postal_code`), or a `location` id. Platforms SHOULD provide one or the other, not both; if both are present, a business SHOULD use the more specific — typically `location`. - """ - - model_config = ConfigDict( - extra="allow", - ) - location: str | None = None - """ - A reference to the destination (e.g. store, pickup location, saved address). - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_destination_update_request.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_destination_update_request.py deleted file mode 100644 index 0ea2cd1..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_destination_update_request.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class FulfillmentDestinationUpdateRequest(BaseModel): - """ - A destination for fulfillment. - """ - - model_config = ConfigDict( - extra="allow", - ) - type: str | None = None - """ - Destination contract discriminator. Required in Business responses and optional in Platform requests. Well-known values: `shipping_address`, `business_location`. The enclosing method contract defines request defaults and which fields the Platform may write; negotiated extensions define additional values. - """ - id: str | None = None - """ - Fulfillment destination identifier. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_event.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_event.py deleted file mode 100644 index 7ae5f99..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_event.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import AnyUrl, AwareDatetime, BaseModel, ConfigDict, Field - - -class LineItem(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Line item ID reference. - """ - quantity: int = Field(..., ge=1, le=9007199254740991) - """ - Integer count of steps of the referenced line item's `quantity_unit` (`10^-scale` × `unit`); when `quantity_unit` is absent, it counts whole items (`each`). - """ - - -class FulfillmentEvent(BaseModel): - """ - Append-only fulfillment event representing an actual shipment. References line items by ID. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Fulfillment event identifier. - """ - occurred_at: AwareDatetime - """ - RFC 3339 timestamp when this fulfillment event occurred. - """ - type: str - """ - Fulfillment event type. Common values include: processing (preparing to ship), shipped (handed to carrier), in_transit (in delivery network), delivered (received by buyer), failed_attempt (delivery attempt failed), canceled (fulfillment canceled), undeliverable (cannot be delivered), returned_to_sender (returned to merchant). - """ - line_items: list[LineItem] - """ - Which line items and quantities are fulfilled in this event. - """ - tracking_number: str | None = None - """ - Carrier tracking number (required if type != processing). - """ - tracking_url: AnyUrl | None = None - """ - URL to track this shipment (required if type != processing). - """ - carrier: str | None = None - """ - Carrier name (e.g., 'FedEx', 'USPS'). - """ - description: str | None = None - """ - Human-readable description of the shipment status or delivery information (e.g., 'Delivered to front door', 'Out for delivery'). - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_event_create_request.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_event_create_request.py deleted file mode 100644 index 633d362..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_event_create_request.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import AnyUrl, AwareDatetime, BaseModel, ConfigDict, Field - - -class LineItem(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Line item ID reference. - """ - quantity: int = Field(..., ge=1, le=9007199254740991) - """ - Integer count of steps of the referenced line item's `quantity_unit` (`10^-scale` × `unit`); when `quantity_unit` is absent, it counts whole items (`each`). - """ - - -class FulfillmentEventCreateRequest(BaseModel): - """ - Append-only fulfillment event representing an actual shipment. References line items by ID. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Fulfillment event identifier. - """ - occurred_at: AwareDatetime - """ - RFC 3339 timestamp when this fulfillment event occurred. - """ - type: str - """ - Fulfillment event type. Common values include: processing (preparing to ship), shipped (handed to carrier), in_transit (in delivery network), delivered (received by buyer), failed_attempt (delivery attempt failed), canceled (fulfillment canceled), undeliverable (cannot be delivered), returned_to_sender (returned to merchant). - """ - line_items: list[LineItem] - """ - Which line items and quantities are fulfilled in this event. - """ - tracking_number: str | None = None - """ - Carrier tracking number (required if type != processing). - """ - tracking_url: AnyUrl | None = None - """ - URL to track this shipment (required if type != processing). - """ - carrier: str | None = None - """ - Carrier name (e.g., 'FedEx', 'USPS'). - """ - description: str | None = None - """ - Human-readable description of the shipment status or delivery information (e.g., 'Delivered to front door', 'Out for delivery'). - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_event_update_request.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_event_update_request.py deleted file mode 100644 index a67550e..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_event_update_request.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import AnyUrl, AwareDatetime, BaseModel, ConfigDict, Field - - -class LineItem(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Line item ID reference. - """ - quantity: int = Field(..., ge=1, le=9007199254740991) - """ - Integer count of steps of the referenced line item's `quantity_unit` (`10^-scale` × `unit`); when `quantity_unit` is absent, it counts whole items (`each`). - """ - - -class FulfillmentEventUpdateRequest(BaseModel): - """ - Append-only fulfillment event representing an actual shipment. References line items by ID. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Fulfillment event identifier. - """ - occurred_at: AwareDatetime - """ - RFC 3339 timestamp when this fulfillment event occurred. - """ - type: str - """ - Fulfillment event type. Common values include: processing (preparing to ship), shipped (handed to carrier), in_transit (in delivery network), delivered (received by buyer), failed_attempt (delivery attempt failed), canceled (fulfillment canceled), undeliverable (cannot be delivered), returned_to_sender (returned to merchant). - """ - line_items: list[LineItem] - """ - Which line items and quantities are fulfilled in this event. - """ - tracking_number: str | None = None - """ - Carrier tracking number (required if type != processing). - """ - tracking_url: AnyUrl | None = None - """ - URL to track this shipment (required if type != processing). - """ - carrier: str | None = None - """ - Carrier name (e.g., 'FedEx', 'USPS'). - """ - description: str | None = None - """ - Human-readable description of the shipment status or delivery information (e.g., 'Delivered to front door', 'Out for delivery'). - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_group.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_group.py deleted file mode 100644 index b3646b8..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_group.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - -from . import fulfillment_option - - -class FulfillmentGroup(BaseModel): - """ - A merchant-generated package/group of line items with fulfillment options. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Group identifier for referencing merchant-generated groups in updates. - """ - line_item_ids: list[str] - """ - Line item IDs included in this group/package. - """ - options: list[fulfillment_option.FulfillmentOption] | None = None - """ - Available fulfillment options for this group. - """ - selected_option_id: str | None = None - """ - ID of the selected fulfillment option for this group. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_group_create_request.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_group_create_request.py deleted file mode 100644 index 99489e4..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_group_create_request.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class FulfillmentGroupCreateRequest(BaseModel): - """ - A merchant-generated package/group of line items with fulfillment options. - """ - - model_config = ConfigDict( - extra="allow", - ) - selected_option_id: str | None = None - """ - ID of the selected fulfillment option for this group. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_group_update_request.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_group_update_request.py deleted file mode 100644 index c830c71..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_group_update_request.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class FulfillmentGroupUpdateRequest(BaseModel): - """ - A merchant-generated package/group of line items with fulfillment options. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Group identifier for referencing merchant-generated groups in updates. - """ - selected_option_id: str | None = None - """ - ID of the selected fulfillment option for this group. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_method.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_method.py index e842dd0..d47cad7 100644 --- a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_method.py +++ b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_method.py @@ -1,127 +1,15 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Fulfillment method models.""" from __future__ import annotations -from pydantic import BaseModel, ConfigDict, model_validator - -from . import fulfillment_destination, fulfillment_group - - -class FulfillmentMethod(BaseModel): - """ - A fulfillment method with destinations and groups. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Unique fulfillment method identifier. - """ - type: str - """ - Fulfillment method type. Well-known values: `shipping`, `pickup`. Businesses MAY use additional values. - """ - line_item_ids: list[str] - """ - Line item IDs fulfilled via this method. - """ - destinations: ( - list[fulfillment_destination.FulfillmentDestination] | None - ) = None - """ - Available destinations for this method. In Business responses, each destination carries a `type` and `id`. - """ - selected_destination_id: str | None = None - """ - ID of the selected destination. Accepts any stable, Business-scoped ID the Business recognizes for this method, including Location IDs not yet enumerated in `destinations`. - """ - groups: list[fulfillment_group.FulfillmentGroup] | None = None - """ - Fulfillment groups for selecting options. Agent sets selected_option_id on groups to choose shipping method. - """ - - @model_validator(mode="after") - def _enforce_conditional_item_retyping(self): - """JSON Schema if/then: approximate a discriminator's array-item - retyping to a different referenced schema, via that schema's own - required keys and const-pinned fields.""" - rules = [ - { - "discriminator": "type", - "values": ["shipping"], - "field": "destinations", - "required": ["id", "type"], - "consts": {"type": "shipping_address"}, - }, - { - "discriminator": "type", - "values": ["pickup"], - "field": "destinations", - "required": ["type"], - "consts": {"type": "business_location"}, - }, - ] - for rule in rules: - actual = getattr(self, rule["discriminator"], None) - if actual not in rule["values"]: - continue - for _item in getattr(self, rule["field"], None) or []: - _provided = ( - set(_item.keys()) - if isinstance(_item, dict) - else _item.model_fields_set | set(_item.model_extra or {}) - ) - for _required in rule["required"]: - if _required not in _provided: - raise ValueError( - f"Field {_required!r} is required for " - f"{rule['field']} items when " - f"{rule['discriminator']} is {actual!r}" - ) - for _const_field, _const_value in rule["consts"].items(): - _actual_value = ( - _item.get(_const_field) - if isinstance(_item, dict) - else getattr(_item, _const_field, None) - ) - if _actual_value != _const_value: - raise ValueError( - f"Field {_const_field!r} must equal " - f"{_const_value!r} for {rule['field']} items " - f"when {rule['discriminator']} is {actual!r}" - ) - return self - - @model_validator(mode="after") - def _enforce_dependent_required(self): - """JSON Schema dependentRequired: enforce dependent fields.""" - rules = {"destinations": ["type"]} - provided = self.model_fields_set | set(self.model_extra or {}) - for field, required_fields in rules.items(): - if field not in provided: - continue - for required in required_fields: - if required not in provided: - raise ValueError( - f"Field {required!r} is required when {field!r} " - "is provided (schema dependentRequired)" - ) - return self +from ...models import ( + FulfillmentMethod, + FulfillmentMethodCreateRequest, + FulfillmentMethodUpdateRequest, +) + +__all__ = [ + "FulfillmentMethod", + "FulfillmentMethodCreateRequest", + "FulfillmentMethodUpdateRequest", +] diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_method_create_request.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_method_create_request.py deleted file mode 100644 index 981c8cd..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_method_create_request.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - -from . import fulfillment_group_create_request - - -class FulfillmentMethodCreateRequest(BaseModel): - """ - A fulfillment method with destinations and groups. - """ - - model_config = ConfigDict( - extra="allow", - ) - type: str - """ - Fulfillment method type. Well-known values: `shipping`, `pickup`. Businesses MAY use additional values. - """ - selected_destination_id: str | None = None - """ - ID of the selected destination. Accepts any stable, Business-scoped ID the Business recognizes for this method, including Location IDs not yet enumerated in `destinations`. - """ - groups: ( - list[fulfillment_group_create_request.FulfillmentGroupCreateRequest] - | None - ) = None - """ - Fulfillment groups for selecting options. Agent sets selected_option_id on groups to choose shipping method. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_method_update_request.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_method_update_request.py deleted file mode 100644 index 3f06a19..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_method_update_request.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - -from . import fulfillment_group_update_request - - -class FulfillmentMethodUpdateRequest(BaseModel): - """ - A fulfillment method with destinations and groups. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str | None = None - """ - Unique fulfillment method identifier. - """ - type: str | None = None - """ - Fulfillment method type. Well-known values: `shipping`, `pickup`. Businesses MAY use additional values. - """ - line_item_ids: list[str] - """ - Line item IDs fulfilled via this method. - """ - selected_destination_id: str | None = None - """ - ID of the selected destination. Accepts any stable, Business-scoped ID the Business recognizes for this method, including Location IDs not yet enumerated in `destinations`. - """ - groups: ( - list[fulfillment_group_update_request.FulfillmentGroupUpdateRequest] - | None - ) = None - """ - Fulfillment groups for selecting options. Agent sets selected_option_id on groups to choose shipping method. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_option.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_option.py deleted file mode 100644 index b5f3ff1..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_option.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import AwareDatetime, ConfigDict - -from ...common.types import total -from .fulfillment_option_base import FulfillmentOptionBase - - -class FulfillmentOption(FulfillmentOptionBase): - """ - A fulfillment option within a group (e.g., Standard Shipping $5, Express $15). Extends the fulfillment option base with cost and timing. - """ - - model_config = ConfigDict( - extra="allow", - ) - carrier: str | None = None - """ - Carrier name (for shipping). - """ - earliest_fulfillment_time: AwareDatetime | None = None - """ - Earliest fulfillment date. - """ - latest_fulfillment_time: AwareDatetime | None = None - """ - Latest fulfillment date. - """ - totals: list[total.Total] - """ - Fulfillment option totals breakdown. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_option_base.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_option_base.py deleted file mode 100644 index b54b7de..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_option_base.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - -from ...common.types import description as description_1 - - -class FulfillmentOptionBase(BaseModel): - """ - Common base for a fulfillment option: an addressable, renderable choice (e.g. Standard, Express). Catalog uses this base directly; checkout composes it with cost and timing. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Unique identifier for this fulfillment option. - """ - title: str - """ - Short label that distinguishes this option from its siblings (e.g. 'Standard', 'Express Shipping', 'Curbside Pickup'). - """ - description: description_1.Description | None = None - """ - Supplementary context for the title (e.g. 'Arrives in 4 business days', 'Arrives Dec 12-15 via FedEx'). Directly renderable; MUST NOT repeat the title. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_option_base_create_request.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_option_base_create_request.py deleted file mode 100644 index 5431e66..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_option_base_create_request.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class FulfillmentOptionBaseCreateRequest(BaseModel): - """ - Common base for a fulfillment option: an addressable, renderable choice (e.g. Standard, Express). Catalog uses this base directly; checkout composes it with cost and timing. - """ - - model_config = ConfigDict( - extra="allow", - ) diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_option_base_update_request.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_option_base_update_request.py deleted file mode 100644 index b0112af..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_option_base_update_request.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class FulfillmentOptionBaseUpdateRequest(BaseModel): - """ - Common base for a fulfillment option: an addressable, renderable choice (e.g. Standard, Express). Catalog uses this base directly; checkout composes it with cost and timing. - """ - - model_config = ConfigDict( - extra="allow", - ) diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_option_create_request.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_option_create_request.py deleted file mode 100644 index 4cbda60..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_option_create_request.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import ConfigDict - -from .fulfillment_option_base_create_request import ( - FulfillmentOptionBaseCreateRequest, -) - - -class FulfillmentOptionCreateRequest(FulfillmentOptionBaseCreateRequest): - """ - A fulfillment option within a group (e.g., Standard Shipping $5, Express $15). Extends the fulfillment option base with cost and timing. - """ - - model_config = ConfigDict( - extra="allow", - ) diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_option_update_request.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_option_update_request.py deleted file mode 100644 index d2928a7..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_option_update_request.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import ConfigDict - -from .fulfillment_option_base_update_request import ( - FulfillmentOptionBaseUpdateRequest, -) - - -class FulfillmentOptionUpdateRequest(FulfillmentOptionBaseUpdateRequest): - """ - A fulfillment option within a group (e.g., Standard Shipping $5, Express $15). Extends the fulfillment option base with cost and timing. - """ - - model_config = ConfigDict( - extra="allow", - ) diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_update_request.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_update_request.py deleted file mode 100644 index 849b10a..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_update_request.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - -from . import fulfillment_method_update_request - - -class FulfillmentUpdateRequest(BaseModel): - """ - Container for fulfillment methods and availability. - """ - - model_config = ConfigDict( - extra="allow", - ) - methods: ( - list[fulfillment_method_update_request.FulfillmentMethodUpdateRequest] - | None - ) = None - """ - Fulfillment methods for cart items. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/input_correlation.py b/src/ucp_sdk/models/schemas/shopping/types/input_correlation.py deleted file mode 100644 index d4707ea..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/input_correlation.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field - - -class InputCorrelation(BaseModel): - """ - Maps a request identifier to the variant it resolved to, with match semantics. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - The identifier from the lookup request that resolved to this variant. - """ - match: str | None = Field(None, examples=["exact", "featured"]) - """ - How the request identifier resolved to this variant. Well-known values: `exact` (input directly identifies this variant, e.g., variant ID, SKU), `featured` (server selected this variant as representative, e.g., product ID resolved to best match). Businesses MAY implement and provide additional resolution strategies. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/item.py b/src/ucp_sdk/models/schemas/shopping/types/item.py index 1d07c83..6e51935 100644 --- a/src/ucp_sdk/models/schemas/shopping/types/item.py +++ b/src/ucp_sdk/models/schemas/shopping/types/item.py @@ -1,55 +1,15 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Item models.""" from __future__ import annotations -from pydantic import AnyUrl, BaseModel, ConfigDict - -from ...common.types import amount -from ...common.types import quantity_unit as quantity_unit_1 -from . import unit_price as unit_price_1 - - -class Item(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - The product identifier, often the SKU, required to resolve the product details associated with this line item. Should be recognized by both the Platform, and the Business. - """ - title: str - """ - Product title. - """ - price: amount.Amount - """ - Unit price in ISO 4217 minor units. Price is the amount per one whole `quantity_unit.unit` (for example, per lb or per hour); when `quantity_unit` is absent, it is per `each`. - """ - quantity_unit: quantity_unit_1.QuantityUnit | None = None - """ - Sale basis this item's `quantity` is denominated in. On an authoritative Business response, absence encodes the default `each` machine identity (`C62`, 0); the Business MUST include this descriptor for every non-`each` response. On Platform requests, omission makes no assertion: the Business interprets `quantity` using the item's authoritative sale basis. If the Platform includes this descriptor, it asserts the unit-descriptor machine identity. The Business MUST compare that machine identity (`unit`, effective `scale`), ignore `display_text` and `increment`, and resolve a mismatch by conversion surfaced as a visible line revision with a warning, or by rejection with a recoverable business outcome; silent reinterpretation is forbidden. An explicit `C62` descriptor at effective scale 0 matches an authoritative basis represented by an absent descriptor. - """ - unit_price: unit_price_1.UnitPrice | None = None - """ - Pricing basis for this item. On an authoritative Business response, the Business MUST include `unit_price` on every line whose pricing basis differs from its sale basis (for example, priced per pound but sold per `each`); presence on a line marks the rate as transactional rather than display-only. When the pricing basis is the sale basis, `item.price` fully denominates the charge and this field MAY be omitted. - """ - image_url: AnyUrl | None = None - """ - Product image URI. - """ +from ...models import ( + Item, + ItemCreateRequest, + ItemUpdateRequest, +) + +__all__ = [ + "Item", + "ItemCreateRequest", + "ItemUpdateRequest", +] diff --git a/src/ucp_sdk/models/schemas/shopping/types/item_create_request.py b/src/ucp_sdk/models/schemas/shopping/types/item_create_request.py deleted file mode 100644 index df9f0cd..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/item_create_request.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - -from ...common.types import quantity_unit_create_request - - -class ItemCreateRequest(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - The product identifier, often the SKU, required to resolve the product details associated with this line item. Should be recognized by both the Platform, and the Business. - """ - quantity_unit: ( - quantity_unit_create_request.QuantityUnitCreateRequest | None - ) = None - """ - Sale basis this item's `quantity` is denominated in. On an authoritative Business response, absence encodes the default `each` machine identity (`C62`, 0); the Business MUST include this descriptor for every non-`each` response. On Platform requests, omission makes no assertion: the Business interprets `quantity` using the item's authoritative sale basis. If the Platform includes this descriptor, it asserts the unit-descriptor machine identity. The Business MUST compare that machine identity (`unit`, effective `scale`), ignore `display_text` and `increment`, and resolve a mismatch by conversion surfaced as a visible line revision with a warning, or by rejection with a recoverable business outcome; silent reinterpretation is forbidden. An explicit `C62` descriptor at effective scale 0 matches an authoritative basis represented by an absent descriptor. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/item_update_request.py b/src/ucp_sdk/models/schemas/shopping/types/item_update_request.py deleted file mode 100644 index 96186f5..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/item_update_request.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - -from ...common.types import quantity_unit_update_request - - -class ItemUpdateRequest(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - The product identifier, often the SKU, required to resolve the product details associated with this line item. Should be recognized by both the Platform, and the Business. - """ - quantity_unit: ( - quantity_unit_update_request.QuantityUnitUpdateRequest | None - ) = None - """ - Sale basis this item's `quantity` is denominated in. On an authoritative Business response, absence encodes the default `each` machine identity (`C62`, 0); the Business MUST include this descriptor for every non-`each` response. On Platform requests, omission makes no assertion: the Business interprets `quantity` using the item's authoritative sale basis. If the Platform includes this descriptor, it asserts the unit-descriptor machine identity. The Business MUST compare that machine identity (`unit`, effective `scale`), ignore `display_text` and `increment`, and resolve a mismatch by conversion surfaced as a visible line revision with a warning, or by rejection with a recoverable business outcome; silent reinterpretation is forbidden. An explicit `C62` descriptor at effective scale 0 matches an authoritative basis represented by an absent descriptor. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/line_item.py b/src/ucp_sdk/models/schemas/shopping/types/line_item.py index cf31595..2b57b97 100644 --- a/src/ucp_sdk/models/schemas/shopping/types/line_item.py +++ b/src/ucp_sdk/models/schemas/shopping/types/line_item.py @@ -1,48 +1,17 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Line item models.""" from __future__ import annotations -from pydantic import BaseModel, ConfigDict, Field - -from ...common.types import total -from . import item as item_1 - - -class LineItem(BaseModel): - """ - Line item object. Expected to use the currency of the parent object. - """ +from ...models import ( + LineItem, + LineItemCreateRequest, + LineItemModel, + LineItemUpdateRequest, +) - model_config = ConfigDict( - extra="allow", - ) - id: str - item: item_1.Item - quantity: int = Field(..., ge=1, le=9007199254740991) - """ - Always an integer step count. On Platform requests, steps use the item's Business-authoritative sale basis; omitting `item.quantity_unit` makes no assertion and does not imply `each`. On Business responses, `item.quantity_unit` describes the basis; if absent, it encodes the `each` machine identity (`C62`, 0) and `quantity` counts whole items. - """ - totals: list[total.Total] - """ - Line item totals breakdown. - """ - parent_id: str | None = None - """ - Parent line item identifier for any nested structures. - """ +__all__ = [ + "LineItem", + "LineItemCreateRequest", + "LineItemModel", + "LineItemUpdateRequest", +] diff --git a/src/ucp_sdk/models/schemas/shopping/types/line_item_create_request.py b/src/ucp_sdk/models/schemas/shopping/types/line_item_create_request.py deleted file mode 100644 index c669bb3..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/line_item_create_request.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field - -from . import item_create_request - - -class LineItemCreateRequest(BaseModel): - """ - Line item object. Expected to use the currency of the parent object. - """ - - model_config = ConfigDict( - extra="allow", - ) - item: item_create_request.ItemCreateRequest - quantity: int = Field(..., ge=1, le=9007199254740991) - """ - Always an integer step count. On Platform requests, steps use the item's Business-authoritative sale basis; omitting `item.quantity_unit` makes no assertion and does not imply `each`. On Business responses, `item.quantity_unit` describes the basis; if absent, it encodes the `each` machine identity (`C62`, 0) and `quantity` counts whole items. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/line_item_update_request.py b/src/ucp_sdk/models/schemas/shopping/types/line_item_update_request.py deleted file mode 100644 index a5c78e4..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/line_item_update_request.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field - -from . import item_update_request - - -class LineItemUpdateRequest(BaseModel): - """ - Line item object. Expected to use the currency of the parent object. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str | None = None - item: item_update_request.ItemUpdateRequest - quantity: int = Field(..., ge=1, le=9007199254740991) - """ - Always an integer step count. On Platform requests, steps use the item's Business-authoritative sale basis; omitting `item.quantity_unit` makes no assertion and does not imply `each`. On Business responses, `item.quantity_unit` describes the basis; if absent, it encodes the `each` machine identity (`C62`, 0) and `quantity` counts whole items. - """ - parent_id: str | None = None - """ - Parent line item identifier for any nested structures. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/location_destination.py b/src/ucp_sdk/models/schemas/shopping/types/location_destination.py deleted file mode 100644 index 9860bac..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/location_destination.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Literal - -from pydantic import ConfigDict - -from ...common.types.location_summary import LocationSummary - - -class BusinessLocationDestination(LocationSummary): - """ - A business location fulfillment destination. Business-authored and response-only: the Platform selects a location via `selected_destination_id` rather than writing destinations. - """ - - model_config = ConfigDict( - extra="allow", - ) - type: Literal["business_location"] - """ - Destination type discriminator. Response-only. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/location_destination_create_request.py b/src/ucp_sdk/models/schemas/shopping/types/location_destination_create_request.py deleted file mode 100644 index 90cb2a5..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/location_destination_create_request.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import ConfigDict - -from ...common.types.location_summary_create_request import ( - LocationSummaryCreateRequest, -) - - -class BusinessLocationDestinationCreateRequest(LocationSummaryCreateRequest): - """ - A business location fulfillment destination. Business-authored and response-only: the Platform selects a location via `selected_destination_id` rather than writing destinations. - """ - - model_config = ConfigDict( - extra="allow", - ) diff --git a/src/ucp_sdk/models/schemas/shopping/types/location_destination_update_request.py b/src/ucp_sdk/models/schemas/shopping/types/location_destination_update_request.py deleted file mode 100644 index 3154a71..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/location_destination_update_request.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import ConfigDict - -from ...common.types.location_summary_update_request import ( - LocationSummaryUpdateRequest, -) - - -class BusinessLocationDestinationUpdateRequest(LocationSummaryUpdateRequest): - """ - A business location fulfillment destination. Business-authored and response-only: the Platform selects a location via `selected_destination_id` rather than writing destinations. - """ - - model_config = ConfigDict( - extra="allow", - ) diff --git a/src/ucp_sdk/models/schemas/shopping/types/location_summary.py b/src/ucp_sdk/models/schemas/shopping/types/location_summary.py new file mode 100644 index 0000000..dcfe99c --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/types/location_summary.py @@ -0,0 +1,15 @@ +"""Location summary models.""" + +from __future__ import annotations + +from ...models import ( + LocationSummary, + LocationSummaryCreateRequest, + LocationSummaryUpdateRequest, +) + +__all__ = [ + "LocationSummary", + "LocationSummaryCreateRequest", + "LocationSummaryUpdateRequest", +] diff --git a/src/ucp_sdk/models/schemas/shopping/types/option_value.py b/src/ucp_sdk/models/schemas/shopping/types/option_value.py index 63f0414..14a52a1 100644 --- a/src/ucp_sdk/models/schemas/shopping/types/option_value.py +++ b/src/ucp_sdk/models/schemas/shopping/types/option_value.py @@ -1,39 +1,17 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Option value models.""" from __future__ import annotations -from pydantic import BaseModel, ConfigDict - - -class OptionValue(BaseModel): - """ - A selectable value for a product option. - """ +from ...models import ( + DetailOptionValue, + OptionValue, + ProductOption, + SelectedOption, +) - model_config = ConfigDict( - extra="allow", - ) - id: str | None = None - """ - Optional server-assigned identifier for this option value. When present in a selected_option, the server SHOULD use it for matching instead of label. - """ - label: str - """ - Display text for this option value (e.g., 'Small', 'Blue'). - """ +__all__ = [ + "OptionValue", + "SelectedOption", + "ProductOption", + "DetailOptionValue", +] diff --git a/src/ucp_sdk/models/schemas/shopping/types/order_confirmation.py b/src/ucp_sdk/models/schemas/shopping/types/order_confirmation.py deleted file mode 100644 index fd0ed22..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/order_confirmation.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import AnyUrl, BaseModel, ConfigDict - - -class OrderConfirmation(BaseModel): - """ - Order details available at the time of checkout completion. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Unique order identifier. - """ - label: str | None = None - """ - Human-readable label for identifying the order. MUST only be provided by the business. - """ - permalink_url: AnyUrl - """ - Permalink to access the order on merchant site. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/order_line_item.py b/src/ucp_sdk/models/schemas/shopping/types/order_line_item.py index 131c550..7ace546 100644 --- a/src/ucp_sdk/models/schemas/shopping/types/order_line_item.py +++ b/src/ucp_sdk/models/schemas/shopping/types/order_line_item.py @@ -1,78 +1,7 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Order line item models.""" from __future__ import annotations -from typing import Literal - -from pydantic import BaseModel, ConfigDict, Field - -from ...common.types import total as total_1 -from . import item as item_1 - - -class Quantity(BaseModel): - """ - Tracks the line item's original, current active, and fulfilled quantities. All three values use the same inherited `item.quantity_unit`. When `item.quantity_unit` is absent on an authoritative order response, each step is one whole item (`each`) under the shared default. - """ - - model_config = ConfigDict( - extra="allow", - ) - original: int | None = Field(None, ge=0, le=9007199254740991) - """ - Quantity from the original checkout, expressed as an integer step count. - """ - total: int = Field(..., ge=0, le=9007199254740991) - """ - Current active quantity after returns, cancellations, or other order changes, expressed as an integer step count. - """ - fulfilled: int = Field(..., ge=0, le=9007199254740991) - """ - Quantity fulfilled so far, expressed as an integer step count. - """ - +from ...models import OrderLineItem -class OrderLineItem(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Line item identifier. - """ - item: item_1.Item - """ - Purchased item data, including identity, price, and sale basis. - """ - quantity: Quantity - """ - Tracks the line item's original, current active, and fulfilled quantities. All three values use the same inherited `item.quantity_unit`. When `item.quantity_unit` is absent on an authoritative order response, each step is one whole item (`each`) under the shared default. - """ - totals: list[total_1.Total] - """ - Line item totals breakdown. - """ - status: Literal["processing", "partial", "fulfilled", "removed"] - """ - Derived status: removed if quantity.total == 0, fulfilled if quantity.total > 0 and quantity.fulfilled == quantity.total, partial if quantity.total > 0 and quantity.fulfilled > 0, otherwise processing. - """ - parent_id: str | None = None - """ - Parent line item identifier for any nested structures. - """ +__all__ = ["OrderLineItem"] diff --git a/src/ucp_sdk/models/schemas/shopping/types/order_line_item_create_request.py b/src/ucp_sdk/models/schemas/shopping/types/order_line_item_create_request.py deleted file mode 100644 index 6e0fe34..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/order_line_item_create_request.py +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Literal - -from pydantic import BaseModel, ConfigDict, Field - -from ...common.types import total_create_request -from . import item_create_request - - -class Quantity(BaseModel): - """ - Tracks the line item's original, current active, and fulfilled quantities. All three values use the same inherited `item.quantity_unit`. When `item.quantity_unit` is absent on an authoritative order response, each step is one whole item (`each`) under the shared default. - """ - - model_config = ConfigDict( - extra="allow", - ) - original: int | None = Field(None, ge=0, le=9007199254740991) - """ - Quantity from the original checkout, expressed as an integer step count. - """ - total: int = Field(..., ge=0, le=9007199254740991) - """ - Current active quantity after returns, cancellations, or other order changes, expressed as an integer step count. - """ - fulfilled: int = Field(..., ge=0, le=9007199254740991) - """ - Quantity fulfilled so far, expressed as an integer step count. - """ - - -class OrderLineItemCreateRequest(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Line item identifier. - """ - item: item_create_request.ItemCreateRequest - """ - Purchased item data, including identity, price, and sale basis. - """ - quantity: Quantity - """ - Tracks the line item's original, current active, and fulfilled quantities. All three values use the same inherited `item.quantity_unit`. When `item.quantity_unit` is absent on an authoritative order response, each step is one whole item (`each`) under the shared default. - """ - totals: list[total_create_request.TotalCreateRequest] - """ - Line item totals breakdown. - """ - status: Literal["processing", "partial", "fulfilled", "removed"] - """ - Derived status: removed if quantity.total == 0, fulfilled if quantity.total > 0 and quantity.fulfilled == quantity.total, partial if quantity.total > 0 and quantity.fulfilled > 0, otherwise processing. - """ - parent_id: str | None = None - """ - Parent line item identifier for any nested structures. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/order_line_item_update_request.py b/src/ucp_sdk/models/schemas/shopping/types/order_line_item_update_request.py deleted file mode 100644 index 7db84e2..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/order_line_item_update_request.py +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Literal - -from pydantic import BaseModel, ConfigDict, Field - -from ...common.types import total_update_request -from . import item_update_request - - -class Quantity(BaseModel): - """ - Tracks the line item's original, current active, and fulfilled quantities. All three values use the same inherited `item.quantity_unit`. When `item.quantity_unit` is absent on an authoritative order response, each step is one whole item (`each`) under the shared default. - """ - - model_config = ConfigDict( - extra="allow", - ) - original: int | None = Field(None, ge=0, le=9007199254740991) - """ - Quantity from the original checkout, expressed as an integer step count. - """ - total: int = Field(..., ge=0, le=9007199254740991) - """ - Current active quantity after returns, cancellations, or other order changes, expressed as an integer step count. - """ - fulfilled: int = Field(..., ge=0, le=9007199254740991) - """ - Quantity fulfilled so far, expressed as an integer step count. - """ - - -class OrderLineItemUpdateRequest(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Line item identifier. - """ - item: item_update_request.ItemUpdateRequest - """ - Purchased item data, including identity, price, and sale basis. - """ - quantity: Quantity - """ - Tracks the line item's original, current active, and fulfilled quantities. All three values use the same inherited `item.quantity_unit`. When `item.quantity_unit` is absent on an authoritative order response, each step is one whole item (`each`) under the shared default. - """ - totals: list[total_update_request.TotalUpdateRequest] - """ - Line item totals breakdown. - """ - status: Literal["processing", "partial", "fulfilled", "removed"] - """ - Derived status: removed if quantity.total == 0, fulfilled if quantity.total > 0 and quantity.fulfilled == quantity.total, partial if quantity.total > 0 and quantity.fulfilled > 0, otherwise processing. - """ - parent_id: str | None = None - """ - Parent line item identifier for any nested structures. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/platform_fulfillment_config.py b/src/ucp_sdk/models/schemas/shopping/types/platform_fulfillment_config.py deleted file mode 100644 index 5757821..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/platform_fulfillment_config.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class PlatformFulfillmentConfig(BaseModel): - """ - Platform's fulfillment configuration. - """ - - model_config = ConfigDict( - extra="allow", - ) - supports_multi_group: bool | None = False - """ - Enables multiple groups per method. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/product.py b/src/ucp_sdk/models/schemas/shopping/types/product.py index 57b9363..49f9b75 100644 --- a/src/ucp_sdk/models/schemas/shopping/types/product.py +++ b/src/ucp_sdk/models/schemas/shopping/types/product.py @@ -1,96 +1,7 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Product models.""" from __future__ import annotations -from typing import Any - -from pydantic import AnyUrl, BaseModel, ConfigDict, Field - -from ...common.types import description as description_1 -from ...common.types import media as media_1 -from ...common.types import price_range as price_range_1 -from . import category, product_option -from . import rating as rating_1 -from . import variant - - -class Product(BaseModel): - """ - A product in the catalog with variants and options. - """ +from ...models import DetailProduct, FulfillmentProduct, Product - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Global ID (GID) uniquely identifying this product. - """ - handle: str | None = None - """ - URL-safe slug for SEO-friendly URLs (e.g., 'blue-runner-pro'). Use id for stable API references. - """ - title: str - """ - Product title. - """ - description: description_1.Description - """ - Product description in one or more formats. - """ - url: AnyUrl | None = None - """ - Canonical product page URL. - """ - categories: list[category.Category] | None = None - """ - Product categories with optional taxonomy identifiers. - """ - price_range: price_range_1.PriceRange - """ - Price range across all variants. - """ - list_price_range: price_range_1.PriceRange | None = None - """ - List price range before discounts (for strikethrough display). - """ - media: list[media_1.Media] | None = None - """ - Product media (images, videos, 3D models). First item is the featured media for listings. - """ - options: list[product_option.ProductOption] | None = None - """ - Product options (Size, Color, etc.). - """ - variants: list[variant.Variant] = Field(..., min_length=1) - """ - Purchasable variants of this product. First item is the featured variant for listings. - """ - rating: rating_1.Rating | None = None - """ - Aggregate product rating. - """ - tags: list[str] | None = None - """ - Product tags for categorization and search. - """ - metadata: dict[str, Any] | None = None - """ - Business-defined custom data extending the standard product model. - """ +__all__ = ["Product", "DetailProduct", "FulfillmentProduct"] diff --git a/src/ucp_sdk/models/schemas/shopping/types/product_option.py b/src/ucp_sdk/models/schemas/shopping/types/product_option.py deleted file mode 100644 index 297717e..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/product_option.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field - -from . import option_value - - -class ProductOption(BaseModel): - """ - A product option such as size, color, or material. - """ - - model_config = ConfigDict( - extra="allow", - ) - name: str - """ - Option name (e.g., 'Size', 'Color'). - """ - values: list[option_value.OptionValue] = Field(..., min_length=1) - """ - Available values for this option. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/rating.py b/src/ucp_sdk/models/schemas/shopping/types/rating.py deleted file mode 100644 index 79388fd..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/rating.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field - - -class Rating(BaseModel): - """ - Product rating aggregate. - """ - - model_config = ConfigDict( - extra="allow", - ) - value: float = Field(..., ge=0.0) - """ - Average rating value. - """ - scale_min: float | None = Field(1, ge=0.0) - """ - Minimum value on the rating scale (e.g., 1 for 1-5 stars). - """ - scale_max: float = Field(..., ge=1.0) - """ - Maximum value on the rating scale (e.g., 5 for 5-star). - """ - count: int | None = Field(None, ge=0) - """ - Number of reviews contributing to the rating. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/search_filters.py b/src/ucp_sdk/models/schemas/shopping/types/search_filters.py deleted file mode 100644 index c57be17..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/search_filters.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - -from ...common.types import price_filter - - -class SearchFilters(BaseModel): - """ - Filter criteria to narrow search results. All specified filters combine with AND logic. - """ - - model_config = ConfigDict( - extra="allow", - ) - categories: list[str] | None = None - """ - Filter by product categories (OR logic — matches products in any listed categories). Values match against the value field in product category entries. Valid values can be discovered from the categories field in search results, merchant documentation, or standard taxonomies that businesses may align with. - """ - price: price_filter.PriceFilter | None = None diff --git a/src/ucp_sdk/models/schemas/shopping/types/selected_option.py b/src/ucp_sdk/models/schemas/shopping/types/selected_option.py deleted file mode 100644 index 0d6b9f4..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/selected_option.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict - - -class SelectedOption(BaseModel): - """ - A specific option selection on a variant (e.g., Size: Large). - """ - - model_config = ConfigDict( - extra="allow", - ) - name: str - """ - Option name (e.g., 'Size'). - """ - id: str | None = None - """ - Optional option value identifier from option_value.id. When present, the server SHOULD use it for matching; name and label remain required for display. - """ - label: str - """ - Selected option label (e.g., 'Large'). - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/shipping_destination.py b/src/ucp_sdk/models/schemas/shopping/types/shipping_destination.py deleted file mode 100644 index f5abc26..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/shipping_destination.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Literal - -from pydantic import ConfigDict - -from ...common.types.postal_address import PostalAddress - - -class ShippingDestination(PostalAddress): - """ - Shipping destination. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - ID specific to this shipping destination. - """ - type: Literal["shipping_address"] - """ - Destination type discriminator. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/shipping_destination_create_request.py b/src/ucp_sdk/models/schemas/shopping/types/shipping_destination_create_request.py deleted file mode 100644 index b654698..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/shipping_destination_create_request.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Literal - -from pydantic import ConfigDict - -from ...common.types.postal_address_create_request import ( - PostalAddressCreateRequest, -) - - -class ShippingDestinationCreateRequest(PostalAddressCreateRequest): - """ - Shipping destination. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str | None = None - """ - ID specific to this shipping destination. - """ - type: Literal["shipping_address"] | None = None - """ - Destination type discriminator. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/shipping_destination_update_request.py b/src/ucp_sdk/models/schemas/shopping/types/shipping_destination_update_request.py deleted file mode 100644 index c3b5904..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/shipping_destination_update_request.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Literal - -from pydantic import ConfigDict - -from ...common.types.postal_address_update_request import ( - PostalAddressUpdateRequest, -) - - -class ShippingDestinationUpdateRequest(PostalAddressUpdateRequest): - """ - Shipping destination. - """ - - model_config = ConfigDict( - extra="allow", - ) - id: str | None = None - """ - ID specific to this shipping destination. - """ - type: Literal["shipping_address"] | None = None - """ - Destination type discriminator. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/unit_price.py b/src/ucp_sdk/models/schemas/shopping/types/unit_price.py deleted file mode 100644 index 9760096..0000000 --- a/src/ucp_sdk/models/schemas/shopping/types/unit_price.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Any - -from pydantic import BaseModel, ConfigDict, Field - -from ...common.types import amount as amount_1 -from ...common.types.measure import Measure as Measure_1 - - -class Measure(Measure_1): - """ - Product quantity in packaging/content (for example, a 750 mL bottle), distinct from `quantity_unit`, which defines the sale basis. Its integer `value` MUST be at least 1. - """ - - model_config = ConfigDict( - extra="allow", - ) - value: Any | None = Field(None, ge=1) - - -class Reference(Measure_1): - """ - Denominator for unit price display (for example, per 100 mL or per 1 kg). Its integer `value` MUST be at least 1. - """ - - model_config = ConfigDict( - extra="allow", - ) - value: Any | None = Field(None, ge=1) - - -class UnitPrice(BaseModel): - """ - Price per standard unit of measurement. MAY be omitted when unit pricing does not apply. `unit_price.currency` MUST equal `price.currency`; the comparator MUST NOT perform currency conversion. `measure.unit` and `reference.unit` MUST be identical; cross-unit conversion is not permitted. Their scales MAY differ; each value represents `value × 10^-scale`. - """ - - model_config = ConfigDict( - extra="allow", - ) - amount: amount_1.Amount - """ - Unit price in ISO 4217 minor units. After satisfying the same-unit invariant, the Business MUST compute the comparator as `(price.amount / (measure.value × 10^-measure.scale)) × (reference.value × 10^-reference.scale)` and round it once to ISO 4217 minor units according to its pricing rules. The returned `unit_price.amount` is authoritative; the Platform MUST NOT recompute or substitute its own result. - """ - currency: str = Field(..., pattern="^[A-Z]{3}$") - """ - ISO 4217 currency code. - """ - measure: Measure - """ - Product quantity in packaging/content (for example, a 750 mL bottle), distinct from `quantity_unit`, which defines the sale basis. Its integer `value` MUST be at least 1. - """ - reference: Reference - """ - Denominator for unit price display (for example, per 100 mL or per 1 kg). Its integer `value` MUST be at least 1. - """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/variant.py b/src/ucp_sdk/models/schemas/shopping/types/variant.py index 4e93a4c..b034b87 100644 --- a/src/ucp_sdk/models/schemas/shopping/types/variant.py +++ b/src/ucp_sdk/models/schemas/shopping/types/variant.py @@ -1,152 +1,7 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""Variant models.""" from __future__ import annotations -from typing import Any - -from pydantic import AnyUrl, BaseModel, ConfigDict - -from ...common.types import description as description_1 -from ...common.types import link -from ...common.types import media as media_1 -from ...common.types import price as price_1 -from ...common.types import quantity_unit as quantity_unit_1 -from . import availability as availability_1 -from . import category -from . import rating as rating_1 -from . import selected_option -from . import unit_price as unit_price_1 - - -class Barcode(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - type: str - """ - Barcode standard. Well-known values: UPC, EAN, ISBN, GTIN, JAN. - """ - value: str - """ - Barcode value. - """ - - -class Seller(BaseModel): - """ - Optional seller context for this variant. - """ - - model_config = ConfigDict( - extra="allow", - ) - name: str | None = None - """ - Seller display name. - """ - links: list[link.Link] | None = None - """ - Seller policy and information links. - """ - - -class Variant(BaseModel): - """ - A purchasable variant of a product with specific option selections. - """ +from ...models import FulfillmentVariant, LookupVariant, Variant - model_config = ConfigDict( - extra="allow", - ) - id: str - """ - Global ID (GID) uniquely identifying this variant. Used as item.id in checkout. - """ - sku: str | None = None - """ - Business-assigned identifier for inventory and fulfillment. - """ - barcodes: list[Barcode] | None = None - """ - Industry-standard product identifiers for cross-reference and correlation. - """ - handle: str | None = None - """ - URL-safe variant handle/slug. - """ - title: str - """ - Variant display title (e.g., 'Blue / Large'). - """ - description: description_1.Description - """ - Variant description in one or more formats. - """ - url: AnyUrl | None = None - """ - Canonical variant page URL. - """ - categories: list[category.Category] | None = None - """ - Variant categories with optional taxonomy identifiers. - """ - price: price_1.Price - """ - Current selling price. Price is the amount per one whole `quantity_unit.unit` (for example, per lb or per hour); when `quantity_unit` is absent, it is per `each`. Line total is `price × quantity × 10^-scale`, computed and rounded once by the Business; `totals` remain authoritative. - """ - quantity_unit: quantity_unit_1.QuantityUnit | None = None - """ - Sale basis this variant's `quantity` is denominated in. The default sale basis is `each`, whose machine identity is (`C62`, 0); `C62` is the UN/CEFACT Rec20 code for one/each. An absent catalog descriptor encodes that default. An `increment` advertises the ordering granularity in steps (for example, `scale` 2 with `increment` 25 sells in 0.25-unit multiples). - """ - list_price: price_1.Price | None = None - """ - List price before discounts (for strikethrough display). - """ - unit_price: unit_price_1.UnitPrice | None = None - """ - Price per standard unit of measurement, for shelf-style comparison display. MAY be omitted when unit pricing does not apply. - """ - availability: availability_1.Availability | None = None - """ - Variant availability for purchase. - """ - options: list[selected_option.SelectedOption] | None = None - """ - Option values that define this variant (e.g., Color: Blue, Size: Large). - """ - media: list[media_1.Media] | None = None - """ - Variant media (images, videos, 3D models). First item is the featured media for listings. - """ - rating: rating_1.Rating | None = None - """ - Variant rating. - """ - tags: list[str] | None = None - """ - Variant tags for categorization and search. - """ - metadata: dict[str, Any] | None = None - """ - Business-defined custom data extending the standard variant model. - """ - seller: Seller | None = None - """ - Optional seller context for this variant. - """ +__all__ = ["Variant", "LookupVariant", "FulfillmentVariant"] diff --git a/src/ucp_sdk/models/schemas/transports/__init__.py b/src/ucp_sdk/models/schemas/transports/__init__.py deleted file mode 100644 index 1252d6b..0000000 --- a/src/ucp_sdk/models/schemas/transports/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable diff --git a/src/ucp_sdk/models/schemas/transports/a2a_message.py b/src/ucp_sdk/models/schemas/transports/a2a_message.py deleted file mode 100644 index 618f658..0000000 --- a/src/ucp_sdk/models/schemas/transports/a2a_message.py +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated, Any, Literal - -from pydantic import AnyUrl, BaseModel, ConfigDict, Field -from typing_extensions import TypeAliasType - -from .jsonrpc import Request, SuccessResponse - - -class Extension(BaseModel): - """ - A2A Agent Card extension advertisement for UCP. - """ - - model_config = ConfigDict( - extra="allow", - ) - uri: AnyUrl - """ - Extension URI. UCP uses its versioned reference URI. - """ - description: str | None = None - params: dict[str, Any] | None = None - """ - Extension parameters such as advertised UCP capabilities. - """ - - -class AgentCard(BaseModel): - """ - A2A Agent Card fragment advertising UCP support through extensions. - """ - - model_config = ConfigDict( - extra="allow", - ) - extensions: list[Extension] = Field(..., min_length=1) - - -class Part(BaseModel): - """ - A2A message part. UCP examples use text parts for natural language and data parts for structured UCP payloads. - """ - - model_config = ConfigDict( - extra="allow", - ) - type: str | None = None - kind: str | None = None - text: str | None = None - data: dict[str, Any] | None = None - """ - Structured data payload. UCP reserves a2a.ucp.* keys for UCP payloads. - """ - - -class Message(BaseModel): - """ - A2A Message carrying natural-language or structured UCP data parts. - """ - - model_config = ConfigDict( - extra="allow", - ) - role: Literal["user", "agent"] - """ - Message sender role. - """ - parts: list[Part] = Field(..., min_length=1) - messageId: str - kind: Literal["message"] - contextId: str - - -class Params(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - message: Message - - -class MessageRequest(Request): - """ - A2A message/send JSON-RPC request whose params carry a UCP-bearing Message from the platform to the business agent. - """ - - model_config = ConfigDict( - extra="allow", - ) - method: Literal["message/send"] | None = None - params: Params - - -class MessageResponse(SuccessResponse): - """ - JSON-RPC success response whose result is an A2A Message from the business agent. - """ - - model_config = ConfigDict( - extra="allow", - ) - result: Message | None = None - - -A2AUcpMessageEnvelope = TypeAliasType( - "A2AUcpMessageEnvelope", - Annotated[ - AgentCard | MessageRequest | MessageResponse, - Field(..., title="A2A UCP Message Envelope"), - ], -) -""" -Minimal A2A envelope shapes used by UCP's A2A checkout binding. This schema validates UCP's transport mapping points — Agent Card extension advertisement, inbound A2A Message requests, and JSON-RPC responses carrying A2A Message results — without attempting to re-specify the full A2A protocol. -""" diff --git a/src/ucp_sdk/models/schemas/transports/embedded_config.py b/src/ucp_sdk/models/schemas/transports/embedded_config.py deleted file mode 100644 index c632826..0000000 --- a/src/ucp_sdk/models/schemas/transports/embedded_config.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Literal - -from pydantic import BaseModel, ConfigDict - - -class EmbeddedTransportConfig(BaseModel): - """ - Per-session configuration for embedded transport binding. Allows businesses to vary EP availability and delegations based on cart contents, agent authorization, or policy. - """ - - model_config = ConfigDict( - extra="allow", - ) - delegate: list[str] | None = None - """ - Delegations the business allows. At service-level, declares available delegations. In UCP responses, confirms accepted delegations for this session. - """ - color_scheme: list[Literal["light", "dark"]] | None = None - """ - Color schemes the business supports. Hosts use ec_color_scheme query parameter to request a scheme from this list. - """ diff --git a/src/ucp_sdk/models/schemas/transports/embedded_message.py b/src/ucp_sdk/models/schemas/transports/embedded_message.py deleted file mode 100644 index 7ffc673..0000000 --- a/src/ucp_sdk/models/schemas/transports/embedded_message.py +++ /dev/null @@ -1,109 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated, Any - -from pydantic import ConfigDict, Field, model_validator -from typing_extensions import TypeAliasType - -from . import jsonrpc -from .jsonrpc import Request as Request_1 -from .jsonrpc import SuccessResponse - -Method = TypeAliasType( - "Method", - Annotated[ - str, - Field( - ..., - pattern="^(ec|ep\\.cart)\\.[a-z][a-z0-9_]*(?:\\.[a-z][a-z0-9_]*)*$", - ), - ], -) -""" -Embedded Protocol method name. Checkout methods use ec.* and cart methods use ep.cart.*. -""" - - -class Request(Request_1): - """ - Embedded Protocol request or notification envelope. Messages with id expect a response; messages without id are notifications. - """ - - model_config = ConfigDict( - extra="allow", - ) - method: Method | None = None - params: dict[str, Any] - """ - Capability-specific EP parameters. - """ - - -class Response(SuccessResponse): - """ - Embedded Protocol success response envelope. UCP application-level success and error outcomes are both carried in result.ucp.status. - """ - - model_config = ConfigDict( - extra="allow", - ) - result: dict[str, Any] | None = None - """ - Capability-specific EP result. Application-level status is defined by capability schemas; see Embedded Protocol response handling. - """ - - @model_validator(mode="after") - def _enforce_conditional_required(self): - """JSON Schema if/then: enforce conditionally required fields.""" - rules = [ - { - "discriminator": "has_next_page", - "values": [True], - "required": ["cursor"], - } - ] - for rule in rules: - if getattr(self, rule["discriminator"], None) not in rule["values"]: - continue - for field in rule["required"]: - if field not in self.model_fields_set: - raise ValueError( - f"Field {field!r} is required by a schema condition" - ) - return self - - -ErrorResponse = TypeAliasType("ErrorResponse", jsonrpc.ErrorResponse) -""" -JSON-RPC transport-level error response for EP messages. Application-level failures use the response result with result.ucp.status=error instead. -""" - - -EmbeddedProtocolMessageEnvelope = TypeAliasType( - "EmbeddedProtocolMessageEnvelope", - Annotated[ - Request | Response | ErrorResponse, - Field(..., title="Embedded Protocol Message Envelope"), - ], -) -""" -JSON-RPC envelope for UCP Embedded Protocol (EP) messages exchanged between a host and an embedded context. This schema constrains the shared transport envelope and method namespace while leaving capability-specific params and result payloads to their capability schemas. -""" diff --git a/src/ucp_sdk/models/schemas/transports/jsonrpc.py b/src/ucp_sdk/models/schemas/transports/jsonrpc.py deleted file mode 100644 index f8ffdd3..0000000 --- a/src/ucp_sdk/models/schemas/transports/jsonrpc.py +++ /dev/null @@ -1,121 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated, Any, Literal - -from pydantic import BaseModel, ConfigDict, Field -from typing_extensions import TypeAliasType - -Id = TypeAliasType("Id", str | float | None) -""" -JSON-RPC request identifier. Notifications omit id; responses echo the request id, or use null when the request id could not be determined. -""" - - -class Error(BaseModel): - """ - JSON-RPC transport-level error object. UCP business outcomes use result payloads with UCP messages instead of this object. - """ - - model_config = ConfigDict( - extra="allow", - ) - code: int - """ - JSON-RPC error code. Standard codes are negative integers; UCP bindings reserve business errors for UCP messages. - """ - message: str - """ - Short transport-level error description. - """ - data: Any | None = None - """ - Optional machine-readable transport error details. - """ - - -class Request(BaseModel): - """ - JSON-RPC request or notification envelope. Presence of id makes the message a request; absence of id makes it a notification. - """ - - model_config = ConfigDict( - extra="allow", - ) - jsonrpc: Literal["2.0"] - """ - JSON-RPC protocol version. - """ - id: Id | None = None - method: str = Field(..., min_length=1) - """ - Transport method name. Binding-specific schemas constrain the method namespace. - """ - params: dict[str, Any] | list[Any] | None = None - """ - Method parameters. Binding-specific schemas define the object shape. - """ - - -class SuccessResponse(BaseModel): - """ - JSON-RPC success response envelope. - """ - - model_config = ConfigDict( - extra="allow", - ) - jsonrpc: Literal["2.0"] - """ - JSON-RPC protocol version. - """ - id: Id - result: Any - """ - Successful transport result. UCP bindings define the nested result payload. - """ - - -class ErrorResponse(BaseModel): - """ - JSON-RPC transport error response envelope. This is for protocol-level failures, not UCP application-level messages. - """ - - model_config = ConfigDict( - extra="forbid", - ) - jsonrpc: Literal["2.0"] - """ - JSON-RPC protocol version. - """ - id: Id - error: Error - - -JsonRpc20Envelope = TypeAliasType( - "JsonRpc20Envelope", - Annotated[ - Request | SuccessResponse | ErrorResponse, - Field(..., title="JSON-RPC 2.0 Envelope"), - ], -) -""" -Common JSON-RPC 2.0 transport envelope used by UCP JSON-RPC-based bindings. This schema intentionally validates only the protocol envelope; binding-specific params and result payloads are validated by transport-specific schemas or extracted UCP payload schemas. -""" diff --git a/src/ucp_sdk/models/schemas/transports/mcp_tool_call.py b/src/ucp_sdk/models/schemas/transports/mcp_tool_call.py deleted file mode 100644 index 6418787..0000000 --- a/src/ucp_sdk/models/schemas/transports/mcp_tool_call.py +++ /dev/null @@ -1,157 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated, Any, Literal - -from pydantic import BaseModel, ConfigDict, Field, model_validator -from typing_extensions import TypeAliasType - -from . import jsonrpc -from .jsonrpc import Request as Request_1 -from .jsonrpc import SuccessResponse - - -class UcpAgent(BaseModel): - """ - UCP-Agent metadata carried inside MCP tool arguments. - """ - - model_config = ConfigDict( - extra="allow", - ) - profile: str - """ - Platform profile URI advertised to the business. - """ - - -class Meta(BaseModel): - """ - UCP request metadata passed through MCP params.arguments.meta. - """ - - model_config = ConfigDict( - extra="allow", - ) - ucp_agent: UcpAgent | None = Field(None, alias="ucp-agent") - idempotency_key: str | None = Field(None, alias="idempotency-key") - """ - Optional idempotency key for retry-safe mutating operations. - """ - - -class Arguments(BaseModel): - """ - MCP tool arguments. UCP reserves meta for transport metadata; operation payload fields such as checkout, cart, order id, or catalog inputs are operation-specific. - """ - - model_config = ConfigDict( - extra="allow", - ) - meta: Meta | None = None - - -class Params(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - name: str = Field(..., min_length=1) - """ - MCP tool name matching the UCP operation binding, such as create_checkout or get_cart. - """ - arguments: Arguments - - -class Request(Request_1): - """ - MCP tools/call request envelope for invoking a UCP operation. - """ - - model_config = ConfigDict( - extra="allow", - ) - method: Literal["tools/call"] | None = None - params: Params - - -class ContentPart(BaseModel): - """ - MCP content part returned for clients that do not consume structuredContent. - """ - - model_config = ConfigDict( - extra="allow", - ) - type: str - text: str | None = None - - -class Result(BaseModel): - model_config = ConfigDict( - extra="allow", - ) - structuredContent: dict[str, Any] - """ - The UCP response payload for the operation. - """ - content: list[ContentPart] | None = None - - -class Response(SuccessResponse): - """ - MCP tools/call response envelope. UCP payloads are carried in result.structuredContent; content is compatibility output. - """ - - model_config = ConfigDict( - extra="allow", - ) - result: Result | None = None - - @model_validator(mode="after") - def _enforce_conditional_required(self): - """JSON Schema if/then: enforce conditionally required fields.""" - rules = [ - { - "discriminator": "has_next_page", - "values": [True], - "required": ["cursor"], - } - ] - for rule in rules: - if getattr(self, rule["discriminator"], None) not in rule["values"]: - continue - for field in rule["required"]: - if field not in self.model_fields_set: - raise ValueError( - f"Field {field!r} is required by a schema condition" - ) - return self - - -McpToolCallEnvelope = TypeAliasType( - "McpToolCallEnvelope", - Annotated[ - Request | Response | jsonrpc.ErrorResponse, - Field(..., title="MCP Tool Call Envelope"), - ], -) -""" -UCP's MCP transport envelope for JSON-RPC tools/call messages. The schema validates the MCP mapping layer: operation name in params.name, UCP metadata and domain arguments in params.arguments, and UCP output in result.structuredContent. -""" diff --git a/src/ucp_sdk/models/schemas/ucp.py b/src/ucp_sdk/models/schemas/ucp.py index 339727a..3542651 100644 --- a/src/ucp_sdk/models/schemas/ucp.py +++ b/src/ucp_sdk/models/schemas/ucp.py @@ -1,392 +1,23 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable +"""UCP core models.""" from __future__ import annotations -from typing import Annotated, Any, Literal - -from pydantic import AnyUrl, BaseModel, ConfigDict, Field -from typing_extensions import TypeAliasType - -from . import capability, payment_handler, service -from .common.types import request_constraints as request_constraints_1 -from .common.types import reverse_domain_name - -Version = TypeAliasType( - "Version", Annotated[str, Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$")] +from .models import ( + Ucp, + UcpBase, + UcpBusinessSchema, + UcpCreateRequest, + UcpEntity, + UcpPlatformSchema, + UcpUpdateRequest, ) -""" -Version identifier in YYYY-MM-DD format. -""" - - -class VersionConstraint(BaseModel): - """ - Version range requirement with minimum and optional maximum. - """ - - model_config = ConfigDict( - extra="allow", - ) - min: Version - """ - Minimum required version (inclusive). - """ - max: Version | None = None - """ - Maximum compatible version (inclusive). When absent, no upper bound. - """ - - -class Requires(BaseModel): - """ - Version requirements for extension schemas. Declares minimum (and optionally maximum) protocol and capability versions needed for correct operation. - """ - - model_config = ConfigDict( - extra="allow", - ) - protocol: VersionConstraint | None = None - """ - Required range for the selected `ucp.version`. - """ - capabilities: ( - dict[reverse_domain_name.ReverseDomainName, VersionConstraint] | None - ) = None - """ - Required capability versions, keyed by capability name. Keys must be a subset of the extension's $defs keys. - """ - - -MapOrder = TypeAliasType("MapOrder", dict[str, list[str]]) -""" -Preferred key order for map-valued fields in the scope annotated by the containing `ucp` member. Each property names a target map, and its array lists target keys in preferred order. Lists may be partial and are not allowlists. -""" - - -class Entity(BaseModel): - """ - Shared foundation for all UCP entities. - """ - - model_config = ConfigDict( - extra="allow", - ) - version: Version - """ - Entity version in YYYY-MM-DD format. - """ - spec: AnyUrl | None = None - """ - URL to human-readable specification document. - """ - schema_: AnyUrl | None = Field(None, alias="schema") - """ - URL to JSON Schema defining this entity's structure and payloads. - """ - id: str | None = None - """ - Unique identifier for this entity instance. Used to disambiguate when multiple instances exist. - """ - config: dict[str, Any] | None = None - """ - Entity-specific configuration. Structure defined by each entity's schema. - """ - - -class Members(BaseModel): - """ - Members defined inside the reserved `ucp` protocol object. The object is open for forward compatibility: consumers MUST ignore unrecognized members. Only UCP core defines members, and every defined member MUST be safe to ignore. - """ - - model_config = ConfigDict( - extra="allow", - ) - map_order: MapOrder | None = None - request_constraints: request_constraints_1.RequestConstraints | None = None - - -class Base(BaseModel): - """ - Base UCP metadata with shared properties for all schema types. - """ - - model_config = ConfigDict( - extra="allow", - ) - version: Version - map_order: MapOrder | None = None - """ - Preferred key-traversal order for sibling registry fields inside the root `ucp` envelope (`services`, `capabilities`, and `payment_handlers`). - """ - status: Literal["success", "error"] | None = "success" - """ - Application-level status of the UCP operation. - """ - services: ( - dict[reverse_domain_name.ReverseDomainName, list[service.Base]] | None - ) = None - """ - Service registry keyed by reverse-domain name. - """ - capabilities: ( - dict[reverse_domain_name.ReverseDomainName, list[capability.Base]] - | None - ) = None - """ - Capability registry keyed by reverse-domain name. - """ - payment_handlers: ( - dict[reverse_domain_name.ReverseDomainName, list[payment_handler.Base]] - | None - ) = None - """ - Payment handler registry keyed by reverse-domain name. - """ - - -class Success(Base): - """ - UCP metadata with status 'success'. Use for response branches that carry the expected payload. - """ - - model_config = ConfigDict( - extra="allow", - ) - status: Literal["success"] - """ - Application-level status of the UCP operation. - """ - -class Error(Base): - """ - UCP metadata with status 'error'. Use for response branches that carry error information. - """ - - model_config = ConfigDict( - extra="allow", - ) - status: Literal["error"] - """ - Application-level status of the UCP operation. - """ - - -class PlatformSchema(Base): - """ - Full UCP metadata for platform-level configuration. Hosted at a URI advertised by the platform in request headers. - """ - - model_config = ConfigDict( - extra="allow", - ) - services: dict[ - reverse_domain_name.ReverseDomainName, list[service.PlatformSchema6] - ] - """ - Service registry keyed by reverse-domain name. - """ - capabilities: ( - dict[ - reverse_domain_name.ReverseDomainName, - list[capability.PlatformSchema], - ] - | None - ) = None - """ - Capability registry keyed by reverse-domain name. - """ - payment_handlers: dict[ - reverse_domain_name.ReverseDomainName, - list[payment_handler.PlatformSchema], - ] - """ - Payment handler registry keyed by reverse-domain name. - """ - - -class BusinessSchema(Base): - """ - UCP metadata for business/merchant-level configuration. Subset of platform schema with business-specific settings. - """ - - model_config = ConfigDict( - extra="allow", - ) - supported_versions: dict[Version, AnyUrl] | None = None - """ - Previous protocol versions this business supports, mapped to profile URIs. Businesses that support older protocol versions SHOULD advertise each version and link to its profile. Each URI points to a complete, self-contained profile for that version. When omitted, only `version` is supported. - """ - services: dict[ - reverse_domain_name.ReverseDomainName, list[service.BusinessSchema3] - ] - """ - Service registry keyed by reverse-domain name. - """ - capabilities: ( - dict[ - reverse_domain_name.ReverseDomainName, - list[capability.BusinessSchema], - ] - | None - ) = None - """ - Capability registry keyed by reverse-domain name. - """ - payment_handlers: dict[ - reverse_domain_name.ReverseDomainName, - list[payment_handler.BusinessSchema], - ] - """ - Payment handler registry keyed by reverse-domain name. - """ - - -class ResponseCheckoutSchema(Base): - """ - UCP metadata for checkout responses. - """ - - model_config = ConfigDict( - extra="allow", - ) - services: ( - dict[ - reverse_domain_name.ReverseDomainName, list[service.ResponseSchema2] - ] - | None - ) = None - """ - Service registry keyed by reverse-domain name. - """ - capabilities: ( - dict[ - reverse_domain_name.ReverseDomainName, - list[capability.ResponseSchema], - ] - | None - ) = None - """ - Capability registry keyed by reverse-domain name. - """ - payment_handlers: dict[ - reverse_domain_name.ReverseDomainName, - list[payment_handler.ResponseSchema], - ] - """ - Payment handler registry keyed by reverse-domain name. - """ - - -class ResponseOrderSchema(Base): - """ - UCP metadata for order responses. No payment handlers needed post-purchase. - """ - - model_config = ConfigDict( - extra="allow", - ) - capabilities: ( - dict[ - reverse_domain_name.ReverseDomainName, - list[capability.ResponseSchema], - ] - | None - ) = None - """ - Capability registry keyed by reverse-domain name. - """ - - -class ResponseCartSchema(Base): - """ - UCP metadata for cart responses. No payment handlers needed pre-checkout. - """ - - model_config = ConfigDict( - extra="allow", - ) - capabilities: ( - dict[ - reverse_domain_name.ReverseDomainName, - list[capability.ResponseSchema], - ] - | None - ) = None - """ - Capability registry keyed by reverse-domain name. - """ - - -class ResponseCatalogSchema(Base): - """ - UCP metadata for catalog responses. - """ - - model_config = ConfigDict( - extra="allow", - ) - capabilities: ( - dict[ - reverse_domain_name.ReverseDomainName, - list[capability.ResponseSchema], - ] - | None - ) = None - """ - Capability registry keyed by reverse-domain name. - """ - - -class ResponseLocationSchema(Base): - """ - UCP metadata for location responses. - """ - - model_config = ConfigDict( - extra="allow", - ) - capabilities: ( - dict[ - reverse_domain_name.ReverseDomainName, - list[capability.ResponseSchema], - ] - | None - ) = None - """ - Capability registry keyed by reverse-domain name. - """ - - -UcpMetadata = TypeAliasType( - "UcpMetadata", - Annotated[ - PlatformSchema - | BusinessSchema - | ResponseCheckoutSchema - | ResponseOrderSchema - | ResponseCartSchema - | ResponseCatalogSchema - | ResponseLocationSchema, - Field(..., title="UCP Metadata"), - ], -) -""" -Protocol metadata for discovery profiles and responses. Uses slim schema pattern with context-specific required fields. -""" +__all__ = [ + "Ucp", + "UcpBase", + "UcpBusinessSchema", + "UcpPlatformSchema", + "UcpEntity", + "UcpCreateRequest", + "UcpUpdateRequest", +] diff --git a/src/ucp_sdk/models/schemas/ucp_create_request.py b/src/ucp_sdk/models/schemas/ucp_create_request.py deleted file mode 100644 index 7ff439b..0000000 --- a/src/ucp_sdk/models/schemas/ucp_create_request.py +++ /dev/null @@ -1,409 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated, Any, Literal - -from pydantic import AnyUrl, BaseModel, ConfigDict, Field -from typing_extensions import TypeAliasType - -from . import capability, payment_handler, service -from .common.types import request_constraints as request_constraints_1 -from .common.types import reverse_domain_name_create_request - -Version = TypeAliasType( - "Version", Annotated[str, Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$")] -) -""" -Version identifier in YYYY-MM-DD format. -""" - - -class VersionConstraint(BaseModel): - """ - Version range requirement with minimum and optional maximum. - """ - - model_config = ConfigDict( - extra="allow", - ) - min: Version - """ - Minimum required version (inclusive). - """ - max: Version | None = None - """ - Maximum compatible version (inclusive). When absent, no upper bound. - """ - - -class Requires(BaseModel): - """ - Version requirements for extension schemas. Declares minimum (and optionally maximum) protocol and capability versions needed for correct operation. - """ - - model_config = ConfigDict( - extra="allow", - ) - protocol: VersionConstraint | None = None - """ - Required range for the selected `ucp.version`. - """ - capabilities: ( - dict[ - reverse_domain_name_create_request.ReverseDomainNameCreateRequest, - VersionConstraint, - ] - | None - ) = None - """ - Required capability versions, keyed by capability name. Keys must be a subset of the extension's $defs keys. - """ - - -MapOrder = TypeAliasType("MapOrder", dict[str, list[str]]) -""" -Preferred key order for map-valued fields in the scope annotated by the containing `ucp` member. Each property names a target map, and its array lists target keys in preferred order. Lists may be partial and are not allowlists. -""" - - -class Entity(BaseModel): - """ - Shared foundation for all UCP entities. - """ - - model_config = ConfigDict( - extra="allow", - ) - version: Version - """ - Entity version in YYYY-MM-DD format. - """ - spec: AnyUrl | None = None - """ - URL to human-readable specification document. - """ - schema_: AnyUrl | None = Field(None, alias="schema") - """ - URL to JSON Schema defining this entity's structure and payloads. - """ - id: str | None = None - """ - Unique identifier for this entity instance. Used to disambiguate when multiple instances exist. - """ - config: dict[str, Any] | None = None - """ - Entity-specific configuration. Structure defined by each entity's schema. - """ - - -class Members(BaseModel): - """ - Members defined inside the reserved `ucp` protocol object. The object is open for forward compatibility: consumers MUST ignore unrecognized members. Only UCP core defines members, and every defined member MUST be safe to ignore. - """ - - model_config = ConfigDict( - extra="allow", - ) - map_order: MapOrder | None = None - request_constraints: request_constraints_1.RequestConstraints | None = None - - -class Base(BaseModel): - """ - Base UCP metadata with shared properties for all schema types. - """ - - model_config = ConfigDict( - extra="allow", - ) - version: Version - map_order: MapOrder | None = None - """ - Preferred key-traversal order for sibling registry fields inside the root `ucp` envelope (`services`, `capabilities`, and `payment_handlers`). - """ - status: Literal["success", "error"] | None = "success" - """ - Application-level status of the UCP operation. - """ - services: ( - dict[ - reverse_domain_name_create_request.ReverseDomainNameCreateRequest, - list[service.Base], - ] - | None - ) = None - """ - Service registry keyed by reverse-domain name. - """ - capabilities: ( - dict[ - reverse_domain_name_create_request.ReverseDomainNameCreateRequest, - list[capability.Base], - ] - | None - ) = None - """ - Capability registry keyed by reverse-domain name. - """ - payment_handlers: ( - dict[ - reverse_domain_name_create_request.ReverseDomainNameCreateRequest, - list[payment_handler.Base], - ] - | None - ) = None - """ - Payment handler registry keyed by reverse-domain name. - """ - - -class Success(Base): - """ - UCP metadata with status 'success'. Use for response branches that carry the expected payload. - """ - - model_config = ConfigDict( - extra="allow", - ) - status: Literal["success"] - """ - Application-level status of the UCP operation. - """ - - -class Error(Base): - """ - UCP metadata with status 'error'. Use for response branches that carry error information. - """ - - model_config = ConfigDict( - extra="allow", - ) - status: Literal["error"] - """ - Application-level status of the UCP operation. - """ - - -class PlatformSchema(Base): - """ - Full UCP metadata for platform-level configuration. Hosted at a URI advertised by the platform in request headers. - """ - - model_config = ConfigDict( - extra="allow", - ) - services: dict[ - reverse_domain_name_create_request.ReverseDomainNameCreateRequest, - list[service.PlatformSchema6], - ] - """ - Service registry keyed by reverse-domain name. - """ - capabilities: ( - dict[ - reverse_domain_name_create_request.ReverseDomainNameCreateRequest, - list[capability.PlatformSchema], - ] - | None - ) = None - """ - Capability registry keyed by reverse-domain name. - """ - payment_handlers: dict[ - reverse_domain_name_create_request.ReverseDomainNameCreateRequest, - list[payment_handler.PlatformSchema], - ] - """ - Payment handler registry keyed by reverse-domain name. - """ - - -class BusinessSchema(Base): - """ - UCP metadata for business/merchant-level configuration. Subset of platform schema with business-specific settings. - """ - - model_config = ConfigDict( - extra="allow", - ) - supported_versions: dict[Version, AnyUrl] | None = None - """ - Previous protocol versions this business supports, mapped to profile URIs. Businesses that support older protocol versions SHOULD advertise each version and link to its profile. Each URI points to a complete, self-contained profile for that version. When omitted, only `version` is supported. - """ - services: dict[ - reverse_domain_name_create_request.ReverseDomainNameCreateRequest, - list[service.BusinessSchema3], - ] - """ - Service registry keyed by reverse-domain name. - """ - capabilities: ( - dict[ - reverse_domain_name_create_request.ReverseDomainNameCreateRequest, - list[capability.BusinessSchema], - ] - | None - ) = None - """ - Capability registry keyed by reverse-domain name. - """ - payment_handlers: dict[ - reverse_domain_name_create_request.ReverseDomainNameCreateRequest, - list[payment_handler.BusinessSchema], - ] - """ - Payment handler registry keyed by reverse-domain name. - """ - - -class ResponseCheckoutSchema(Base): - """ - UCP metadata for checkout responses. - """ - - model_config = ConfigDict( - extra="allow", - ) - services: ( - dict[ - reverse_domain_name_create_request.ReverseDomainNameCreateRequest, - list[service.ResponseSchema2], - ] - | None - ) = None - """ - Service registry keyed by reverse-domain name. - """ - capabilities: ( - dict[ - reverse_domain_name_create_request.ReverseDomainNameCreateRequest, - list[capability.ResponseSchema], - ] - | None - ) = None - """ - Capability registry keyed by reverse-domain name. - """ - payment_handlers: dict[ - reverse_domain_name_create_request.ReverseDomainNameCreateRequest, - list[payment_handler.ResponseSchema], - ] - """ - Payment handler registry keyed by reverse-domain name. - """ - - -class ResponseOrderSchema(Base): - """ - UCP metadata for order responses. No payment handlers needed post-purchase. - """ - - model_config = ConfigDict( - extra="allow", - ) - capabilities: ( - dict[ - reverse_domain_name_create_request.ReverseDomainNameCreateRequest, - list[capability.ResponseSchema], - ] - | None - ) = None - """ - Capability registry keyed by reverse-domain name. - """ - - -class ResponseCartSchema(Base): - """ - UCP metadata for cart responses. No payment handlers needed pre-checkout. - """ - - model_config = ConfigDict( - extra="allow", - ) - capabilities: ( - dict[ - reverse_domain_name_create_request.ReverseDomainNameCreateRequest, - list[capability.ResponseSchema], - ] - | None - ) = None - """ - Capability registry keyed by reverse-domain name. - """ - - -class ResponseCatalogSchema(Base): - """ - UCP metadata for catalog responses. - """ - - model_config = ConfigDict( - extra="allow", - ) - capabilities: ( - dict[ - reverse_domain_name_create_request.ReverseDomainNameCreateRequest, - list[capability.ResponseSchema], - ] - | None - ) = None - """ - Capability registry keyed by reverse-domain name. - """ - - -class ResponseLocationSchema(Base): - """ - UCP metadata for location responses. - """ - - model_config = ConfigDict( - extra="allow", - ) - capabilities: ( - dict[ - reverse_domain_name_create_request.ReverseDomainNameCreateRequest, - list[capability.ResponseSchema], - ] - | None - ) = None - """ - Capability registry keyed by reverse-domain name. - """ - - -UcpMetadataCreateRequest = TypeAliasType( - "UcpMetadataCreateRequest", - Annotated[ - PlatformSchema - | BusinessSchema - | ResponseCheckoutSchema - | ResponseOrderSchema - | ResponseCartSchema - | ResponseCatalogSchema - | ResponseLocationSchema, - Field(..., title="UCP Metadata Create Request"), - ], -) -""" -Protocol metadata for discovery profiles and responses. Uses slim schema pattern with context-specific required fields. -""" diff --git a/src/ucp_sdk/models/schemas/ucp_update_request.py b/src/ucp_sdk/models/schemas/ucp_update_request.py deleted file mode 100644 index bfad2be..0000000 --- a/src/ucp_sdk/models/schemas/ucp_update_request.py +++ /dev/null @@ -1,409 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# generated by datamodel-codegen -# pylint: disable=all -# pyformat: disable - -from __future__ import annotations - -from typing import Annotated, Any, Literal - -from pydantic import AnyUrl, BaseModel, ConfigDict, Field -from typing_extensions import TypeAliasType - -from . import capability, payment_handler, service -from .common.types import request_constraints as request_constraints_1 -from .common.types import reverse_domain_name_update_request - -Version = TypeAliasType( - "Version", Annotated[str, Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$")] -) -""" -Version identifier in YYYY-MM-DD format. -""" - - -class VersionConstraint(BaseModel): - """ - Version range requirement with minimum and optional maximum. - """ - - model_config = ConfigDict( - extra="allow", - ) - min: Version - """ - Minimum required version (inclusive). - """ - max: Version | None = None - """ - Maximum compatible version (inclusive). When absent, no upper bound. - """ - - -class Requires(BaseModel): - """ - Version requirements for extension schemas. Declares minimum (and optionally maximum) protocol and capability versions needed for correct operation. - """ - - model_config = ConfigDict( - extra="allow", - ) - protocol: VersionConstraint | None = None - """ - Required range for the selected `ucp.version`. - """ - capabilities: ( - dict[ - reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, - VersionConstraint, - ] - | None - ) = None - """ - Required capability versions, keyed by capability name. Keys must be a subset of the extension's $defs keys. - """ - - -MapOrder = TypeAliasType("MapOrder", dict[str, list[str]]) -""" -Preferred key order for map-valued fields in the scope annotated by the containing `ucp` member. Each property names a target map, and its array lists target keys in preferred order. Lists may be partial and are not allowlists. -""" - - -class Entity(BaseModel): - """ - Shared foundation for all UCP entities. - """ - - model_config = ConfigDict( - extra="allow", - ) - version: Version - """ - Entity version in YYYY-MM-DD format. - """ - spec: AnyUrl | None = None - """ - URL to human-readable specification document. - """ - schema_: AnyUrl | None = Field(None, alias="schema") - """ - URL to JSON Schema defining this entity's structure and payloads. - """ - id: str | None = None - """ - Unique identifier for this entity instance. Used to disambiguate when multiple instances exist. - """ - config: dict[str, Any] | None = None - """ - Entity-specific configuration. Structure defined by each entity's schema. - """ - - -class Members(BaseModel): - """ - Members defined inside the reserved `ucp` protocol object. The object is open for forward compatibility: consumers MUST ignore unrecognized members. Only UCP core defines members, and every defined member MUST be safe to ignore. - """ - - model_config = ConfigDict( - extra="allow", - ) - map_order: MapOrder | None = None - request_constraints: request_constraints_1.RequestConstraints | None = None - - -class Base(BaseModel): - """ - Base UCP metadata with shared properties for all schema types. - """ - - model_config = ConfigDict( - extra="allow", - ) - version: Version - map_order: MapOrder | None = None - """ - Preferred key-traversal order for sibling registry fields inside the root `ucp` envelope (`services`, `capabilities`, and `payment_handlers`). - """ - status: Literal["success", "error"] | None = "success" - """ - Application-level status of the UCP operation. - """ - services: ( - dict[ - reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, - list[service.Base], - ] - | None - ) = None - """ - Service registry keyed by reverse-domain name. - """ - capabilities: ( - dict[ - reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, - list[capability.Base], - ] - | None - ) = None - """ - Capability registry keyed by reverse-domain name. - """ - payment_handlers: ( - dict[ - reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, - list[payment_handler.Base], - ] - | None - ) = None - """ - Payment handler registry keyed by reverse-domain name. - """ - - -class Success(Base): - """ - UCP metadata with status 'success'. Use for response branches that carry the expected payload. - """ - - model_config = ConfigDict( - extra="allow", - ) - status: Literal["success"] - """ - Application-level status of the UCP operation. - """ - - -class Error(Base): - """ - UCP metadata with status 'error'. Use for response branches that carry error information. - """ - - model_config = ConfigDict( - extra="allow", - ) - status: Literal["error"] - """ - Application-level status of the UCP operation. - """ - - -class PlatformSchema(Base): - """ - Full UCP metadata for platform-level configuration. Hosted at a URI advertised by the platform in request headers. - """ - - model_config = ConfigDict( - extra="allow", - ) - services: dict[ - reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, - list[service.PlatformSchema6], - ] - """ - Service registry keyed by reverse-domain name. - """ - capabilities: ( - dict[ - reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, - list[capability.PlatformSchema], - ] - | None - ) = None - """ - Capability registry keyed by reverse-domain name. - """ - payment_handlers: dict[ - reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, - list[payment_handler.PlatformSchema], - ] - """ - Payment handler registry keyed by reverse-domain name. - """ - - -class BusinessSchema(Base): - """ - UCP metadata for business/merchant-level configuration. Subset of platform schema with business-specific settings. - """ - - model_config = ConfigDict( - extra="allow", - ) - supported_versions: dict[Version, AnyUrl] | None = None - """ - Previous protocol versions this business supports, mapped to profile URIs. Businesses that support older protocol versions SHOULD advertise each version and link to its profile. Each URI points to a complete, self-contained profile for that version. When omitted, only `version` is supported. - """ - services: dict[ - reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, - list[service.BusinessSchema3], - ] - """ - Service registry keyed by reverse-domain name. - """ - capabilities: ( - dict[ - reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, - list[capability.BusinessSchema], - ] - | None - ) = None - """ - Capability registry keyed by reverse-domain name. - """ - payment_handlers: dict[ - reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, - list[payment_handler.BusinessSchema], - ] - """ - Payment handler registry keyed by reverse-domain name. - """ - - -class ResponseCheckoutSchema(Base): - """ - UCP metadata for checkout responses. - """ - - model_config = ConfigDict( - extra="allow", - ) - services: ( - dict[ - reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, - list[service.ResponseSchema2], - ] - | None - ) = None - """ - Service registry keyed by reverse-domain name. - """ - capabilities: ( - dict[ - reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, - list[capability.ResponseSchema], - ] - | None - ) = None - """ - Capability registry keyed by reverse-domain name. - """ - payment_handlers: dict[ - reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, - list[payment_handler.ResponseSchema], - ] - """ - Payment handler registry keyed by reverse-domain name. - """ - - -class ResponseOrderSchema(Base): - """ - UCP metadata for order responses. No payment handlers needed post-purchase. - """ - - model_config = ConfigDict( - extra="allow", - ) - capabilities: ( - dict[ - reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, - list[capability.ResponseSchema], - ] - | None - ) = None - """ - Capability registry keyed by reverse-domain name. - """ - - -class ResponseCartSchema(Base): - """ - UCP metadata for cart responses. No payment handlers needed pre-checkout. - """ - - model_config = ConfigDict( - extra="allow", - ) - capabilities: ( - dict[ - reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, - list[capability.ResponseSchema], - ] - | None - ) = None - """ - Capability registry keyed by reverse-domain name. - """ - - -class ResponseCatalogSchema(Base): - """ - UCP metadata for catalog responses. - """ - - model_config = ConfigDict( - extra="allow", - ) - capabilities: ( - dict[ - reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, - list[capability.ResponseSchema], - ] - | None - ) = None - """ - Capability registry keyed by reverse-domain name. - """ - - -class ResponseLocationSchema(Base): - """ - UCP metadata for location responses. - """ - - model_config = ConfigDict( - extra="allow", - ) - capabilities: ( - dict[ - reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, - list[capability.ResponseSchema], - ] - | None - ) = None - """ - Capability registry keyed by reverse-domain name. - """ - - -UcpMetadataUpdateRequest = TypeAliasType( - "UcpMetadataUpdateRequest", - Annotated[ - PlatformSchema - | BusinessSchema - | ResponseCheckoutSchema - | ResponseOrderSchema - | ResponseCartSchema - | ResponseCatalogSchema - | ResponseLocationSchema, - Field(..., title="UCP Metadata Update Request"), - ], -) -""" -Protocol metadata for discovery profiles and responses. Uses slim schema pattern with context-specific required fields. -""" diff --git a/tests/test_codegen_pipeline.py b/tests/test_codegen_pipeline.py deleted file mode 100644 index 75a65fd..0000000 --- a/tests/test_codegen_pipeline.py +++ /dev/null @@ -1,2295 +0,0 @@ -# Copyright 2026 UCP Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tests for the schema preprocessing pipeline.""" - -import ast -import contextlib -import copy -import io -import json -import tempfile -import unittest -from pathlib import Path - -import postprocess_models - -try: - from pydantic import TypeAdapter, ValidationError - - # NOTE(root-cause-0): these paths moved from shopping.types to - # common.types when #87 (2026-08-25 UCP release regen) restructured the - # schema tree. The old paths silently raise ModuleNotFoundError here, - # which the except clause below swallows as HAVE_SDK = False -- so every - # semantic test gated on HAVE_SDK skips instead of running, and CI is - # green on a suite that mostly never executed. See the sibling fixes to - # the other stale shopping.types.* imports later in this file. - from ucp_sdk.models.schemas.common.types.description import Description - from ucp_sdk.models.schemas.common.types.totals import Totals - from ucp_sdk.models.schemas.common.types.totals_create_request import ( - TotalsCreateRequest, - ) - from ucp_sdk.models.schemas.common.types.totals_update_request import ( - TotalsUpdateRequest, - ) - - HAVE_SDK = True -except (ImportError, SyntaxError): # pragma: no cover - # A generated model with invalid Python (e.g. a postprocessing splice - # that lands beside a stray trailing comma - see - # ArrayContainsInjectorTest.test_injects_cleanly_when_annotated_is_line_wrapped) - # raises SyntaxError on import, not ImportError. Without catching it - # here too, one broken generated file takes the whole test module down - # at collection time and every other test in this file - most of which - # have nothing to do with the SDK build - never runs. - HAVE_SDK = False - - -@unittest.skipUnless( - HAVE_SDK, "requires the installed package (pip install -e .)" -) -class DescriptionMinPropertiesTest(unittest.TestCase): - """description.json declares minProperties: 1 at the schema root.""" - - def test_empty_instance_rejected(self): - with self.assertRaisesRegex(ValidationError, "[Aa]t least 1"): - Description() - - def test_empty_mapping_rejected(self): - with self.assertRaisesRegex(ValidationError, "[Aa]t least 1"): - Description.model_validate({}) - - def test_single_declared_field_accepted(self): - self.assertEqual(Description(plain="hello").plain, "hello") - - def test_explicit_null_key_counts_as_present(self): - # {"html": null} has one property per JSON Schema's key counting. - Description.model_validate({"html": None}) - - def test_extra_field_counts_as_present(self): - # extra="allow": an unknown key is a present property. - Description.model_validate({"x-vendor-note": "hi"}) - - def test_all_fields_accepted(self): - Description(plain="p", html="

p

", markdown="p") - - -@unittest.skipUnless( - HAVE_SDK, "requires the installed package (pip install -e .)" -) -class SignalsPropertyNamesTest(unittest.TestCase): - """signals.json declares propertyNames (reverse-domain keys). - - Signals has named ``properties`` AND ``additionalProperties: true``, so the - generator emits ``class Signals(BaseModel)`` with ``extra="allow"`` and named - fields; extra keys bypass the ``propertyNames`` pattern. The post-generation - injector restores the check on every ``model_extra`` key while preserving - well-formed reverse-domain extras (extra="allow" keeps them). - """ - - def _signals(self): - from ucp_sdk.models.schemas.common.types.signals import Signals - - return Signals - - def test_malformed_extra_key_rejected(self): - with self.assertRaisesRegex(ValidationError, "propertyNames"): - self._signals().model_validate( - {"dev.ucp.buyer_ip": "1.2.3.4", "bogus KEY!": "x"} - ) - - def test_trailing_newline_key_rejected(self): - # A $-anchored pattern with re.match would let a trailing newline - # slip through; the enforcement uses re.fullmatch to agree with - # pydantic-core's key validation on the sibling dict-map path. - with self.assertRaisesRegex(ValidationError, "propertyNames"): - self._signals().model_validate({"com.example.k\n": "x"}) - - def test_valid_reverse_domain_extra_accepted_and_preserved(self): - signals = self._signals().model_validate( - {"com.example.device_id": "abc123"} - ) - # extra="allow" must still keep a well-formed extra key. - self.assertEqual( - signals.model_extra, {"com.example.device_id": "abc123"} - ) - - def test_known_named_fields_still_work(self): - signals = self._signals().model_validate( - { - "dev.ucp.buyer_ip": "1.2.3.4", - "dev.ucp.user_agent": "curl/8", - } - ) - self.assertEqual(signals.dev_ucp_buyer_ip, "1.2.3.4") - self.assertEqual(signals.dev_ucp_user_agent, "curl/8") - self.assertEqual(signals.model_extra, {}) - - def test_request_variants_enforce_property_names(self): - # The gap and its fix travel to the generated request variants too. - from ucp_sdk.models.schemas.common.types.signals_complete_request import ( - SignalsCompleteRequest, - ) - from ucp_sdk.models.schemas.common.types.signals_create_request import ( - SignalsCreateRequest, - ) - from ucp_sdk.models.schemas.common.types.signals_update_request import ( - SignalsUpdateRequest, - ) - - for cls in ( - SignalsCreateRequest, - SignalsUpdateRequest, - SignalsCompleteRequest, - ): - with self.subTest(model=cls.__name__): - with self.assertRaisesRegex(ValidationError, "propertyNames"): - cls.model_validate({"bogus KEY!": "x"}) - self.assertEqual( - cls.model_validate({"com.example.k": "v"}).model_extra, - {"com.example.k": "v"}, - ) - - -@unittest.skipUnless( - HAVE_SDK, "requires the installed package (pip install -e .)" -) -class IdentityLinkingRoleSchemaTest(unittest.TestCase): - """identity_linking.json keeps its role schemas instead of Any aliases. - - The dotted 'dev.ucp.common.identity_linking' def is a capability role - container. Flattening must split it into two generatable defs so the - business role keeps the upstream contract: 'config.scopes' required with - OAuth scope-token keys. - """ - - def _business(self): - from ucp_sdk.models.schemas.common.identity_linking import ( - IdentityLinkingBusinessSchema, - ) - - return IdentityLinkingBusinessSchema - - def _base(self): - return { - "version": "2026-08-25", - "schema": "https://ucp.dev/2026-08-25/schemas/common/identity_linking", - } - - def test_platform_role_schema_exists(self): - from ucp_sdk.models.schemas.common.identity_linking import ( - IdentityLinkingPlatformSchema, - ) - - IdentityLinkingPlatformSchema( - version="2026-08-25", - **{ - "schema": "https://ucp.dev/2026-08-25/schemas/common/identity_linking" - }, - spec="https://ucp.dev/specification/common/identity-linking", - ) - - def test_business_config_with_scopes_accepted(self): - obj = self._business().model_validate( - { - **self._base(), - "config": {"scopes": {"dev.ucp.shopping.order:read": {}}}, - } - ) - self.assertEqual( - list(obj.config.scopes), ["dev.ucp.shopping.order:read"] - ) - self.assertIsNone( - obj.config.scopes["dev.ucp.shopping.order:read"].description - ) - - def test_missing_config_rejected(self): - with self.assertRaisesRegex(ValidationError, "config"): - self._business().model_validate(self._base()) - - def test_config_without_scopes_rejected(self): - with self.assertRaisesRegex(ValidationError, "scopes"): - self._business().model_validate({**self._base(), "config": {}}) - - def test_malformed_scope_key_rejected(self): - with self.assertRaisesRegex(ValidationError, "pattern"): - self._business().model_validate( - {**self._base(), "config": {"scopes": {"BAD": {}}}} - ) - - -class PropertyNamesInjectorTest(unittest.TestCase): - """The propertyNames post-generation injector's own behavior.""" - - PATTERN = "^[a-z][a-z0-9]*(?:\\.[a-z][a-z0-9_]*)+$" - - SCHEMA = { - "title": "Signals", - "type": "object", - "propertyNames": {"pattern": PATTERN}, - "properties": {"dev.ucp.buyer_ip": {"type": "string"}}, - "additionalProperties": True, - } - - # An object with propertyNames but no named properties is a dict-map - # (key type already carries the pattern) — out of scope. - DICT_MAP_SCHEMA = { - "title": "Requires", - "type": "object", - "propertyNames": {"pattern": PATTERN}, - "additionalProperties": {"type": "string"}, - } - - MODULE = ( - "from __future__ import annotations\n" - "\n" - "from pydantic import BaseModel, ConfigDict, Field\n" - "\n" - "\n" - "class Signals(BaseModel):\n" - ' """Signals."""\n' - "\n" - " model_config = ConfigDict(\n" - ' extra="allow",\n' - " )\n" - ' dev_ucp_buyer_ip: str | None = Field(None, alias="dev.ucp.buyer_ip")\n' - ) - - def test_scan_finds_only_extra_allow_object(self): - with tempfile.TemporaryDirectory() as tmp: - (Path(tmp) / "signals.json").write_text(json.dumps(self.SCHEMA)) - (Path(tmp) / "requires.json").write_text( - json.dumps(self.DICT_MAP_SCHEMA) - ) - found = postprocess_models.find_property_names_patterns(Path(tmp)) - self.assertEqual(found, {"Signals": self.PATTERN}) - - def test_scan_resolves_ref_pattern(self): - with tempfile.TemporaryDirectory() as tmp: - (Path(tmp) / "reverse_domain_name.json").write_text( - json.dumps({"type": "string", "pattern": self.PATTERN}) - ) - (Path(tmp) / "thing.json").write_text( - json.dumps( - { - "title": "Thing", - "type": "object", - "propertyNames": {"$ref": "reverse_domain_name.json"}, - "properties": {"a": {"type": "string"}}, - } - ) - ) - found = postprocess_models.find_property_names_patterns(Path(tmp)) - self.assertEqual(found, {"Thing": self.PATTERN}) - - def test_injects_validator_and_imports(self): - out = postprocess_models.inject_property_names( - self.MODULE, "Signals", self.PATTERN - ) - self.assertIn("model_validator", out) - self.assertIn("import re", out) - self.assertIn("propertyNames", out) - - def test_injection_is_idempotent(self): - once = postprocess_models.inject_property_names( - self.MODULE, "Signals", self.PATTERN - ) - twice = postprocess_models.inject_property_names( - once, "Signals", self.PATTERN - ) - self.assertEqual(once, twice) - - @unittest.skipUnless(HAVE_SDK, "executing the module needs pydantic") - def test_injected_validator_enforces_pattern(self): - out = postprocess_models.inject_property_names( - self.MODULE, "Signals", self.PATTERN - ) - namespace: dict = {} - exec(compile(out, "", "exec"), namespace) # noqa: S102 - signals_cls = namespace["Signals"] - with self.assertRaises(ValidationError): - signals_cls.model_validate({"bogus KEY!": "x"}) - # fullmatch (not match) — a trailing newline must not slip through. - with self.assertRaises(ValidationError): - signals_cls.model_validate({"com.example.ok\n": "v"}) - signals_cls.model_validate({"com.example.ok": "v"}) - - -class ConditionalRequiredInjectorTest(unittest.TestCase): - """Simple JSON Schema if/then required constraints are restored.""" - - MODULE = ( - "from __future__ import annotations\n" - "\n" - "from pydantic import BaseModel, ConfigDict\n" - "\n" - "\n" - "class Response(BaseModel):\n" - ' model_config = ConfigDict(extra="allow")\n' - " cursor: str | None = None\n" - " has_next_page: bool\n" - ) - RULES = [ - { - "discriminator": "has_next_page", - "values": [True], - "required": ["cursor"], - } - ] - - def test_schema_scan_maps_nested_definition_to_generated_class(self): - schema = { - "title": "Pagination", - "type": "object", - "$defs": { - "response": { - "type": "object", - "properties": { - "cursor": {"type": "string"}, - "has_next_page": {"type": "boolean"}, - }, - "if": { - "properties": {"has_next_page": {"const": True}}, - "required": ["has_next_page"], - }, - "then": {"required": ["cursor"]}, - } - }, - } - with tempfile.TemporaryDirectory() as tmp: - Path(tmp, "pagination.json").write_text(json.dumps(schema)) - found = postprocess_models.find_conditional_required(Path(tmp)) - self.assertEqual(found, {"Response": self.RULES}) - - def test_schema_scan_skips_else_branches(self): - schema = { - "title": "Response", - "type": "object", - "properties": { - "cursor": {"type": "string"}, - "has_next_page": {"type": "boolean"}, - }, - "if": { - "properties": {"has_next_page": {"const": True}}, - "required": ["has_next_page"], - }, - "then": {"required": ["cursor"]}, - "else": {"required": ["other"]}, - } - with tempfile.TemporaryDirectory() as tmp: - Path(tmp, "response.json").write_text(json.dumps(schema)) - found = postprocess_models.find_conditional_required(Path(tmp)) - self.assertEqual(found, {}) - - def test_schema_scan_threads_enclosing_scope_into_titled_allof_branch( - self, - ): - """An allOf branch with neither its own `properties` nor a type - title of its own is invisible today: the scan only looks at - `node.get("properties")` on the branch itself, so a branch that - relies on the enclosing object's properties (JWK's five if/then - rules, none of which repeat `properties`) is silently dropped with - no warning. And when the branch DOES carry a human-readable - documentation `title` (JWK's branches are each titled, e.g. "EC - keys carry crv, x, y"), the current code adopts that title as the - class name via `_alias_name`, misattributing the rule to a - nonexistent class instead of the enclosing `JwkPublicKey`. This - mirrors profile.json's jwk_public_key def exactly. - """ - schema = { - "$defs": { - "jwk_public_key": { - "type": "object", - "required": ["kid", "kty"], - "properties": { - "kid": {"type": "string"}, - "kty": {"type": "string"}, - "crv": {"type": "string"}, - "x": {"type": "string"}, - "y": {"type": "string"}, - }, - "allOf": [ - { - "title": "EC keys carry crv, x, y", - "if": { - "properties": {"kty": {"const": "EC"}}, - "required": ["kty"], - }, - "then": {"required": ["crv", "x", "y"]}, - }, - { - "title": "OKP keys carry crv, x", - "if": { - "properties": {"kty": {"const": "OKP"}}, - "required": ["kty"], - }, - "then": {"required": ["crv", "x"]}, - }, - ], - } - } - } - with tempfile.TemporaryDirectory() as tmp: - Path(tmp, "profile.json").write_text(json.dumps(schema)) - found = postprocess_models.find_conditional_required(Path(tmp)) - self.assertEqual( - found, - { - "JwkPublicKey": [ - { - "discriminator": "kty", - "values": ["EC"], - "required": ["crv", "x", "y"], - }, - { - "discriminator": "kty", - "values": ["OKP"], - "required": ["crv", "x"], - }, - ] - }, - ) - - def test_injection_is_idempotent(self): - once = postprocess_models.inject_conditional_required( - self.MODULE, "Response", self.RULES - ) - twice = postprocess_models.inject_conditional_required( - once, "Response", self.RULES - ) - self.assertEqual(once, twice) - - @unittest.skipUnless(HAVE_SDK, "executing the module needs pydantic") - def test_injected_validator_enforces_conditional_required(self): - out = postprocess_models.inject_conditional_required( - self.MODULE, "Response", self.RULES - ) - namespace: dict = {} - exec(compile(out, "", "exec"), namespace) # noqa: S102 - response = namespace["Response"] - with self.assertRaises(ValidationError): - response(has_next_page=True) - response(has_next_page=True, cursor="next-page") - response(has_next_page=False) - - -class ConditionalBoundsInjectorTest(unittest.TestCase): - """JSON Schema if/then numeric bounds are restored.""" - - MODULE = ( - "from __future__ import annotations\n" - "\n" - "from pydantic import BaseModel, ConfigDict\n" - "\n" - "\n" - "class Total(BaseModel):\n" - ' model_config = ConfigDict(extra="allow")\n' - " type: str\n" - " amount: int\n" - ) - RULES = [ - { - "discriminator": "type", - "values": ["discount"], - "bounds": {"amount": {"exclusiveMaximum": 0}}, - }, - { - "discriminator": "type", - "values": ["tax"], - "bounds": {"amount": {"minimum": 0}}, - }, - ] - - def _schema(self): - return { - "title": "Total", - "type": "object", - "properties": { - "type": {"type": "string"}, - "amount": {"type": "integer"}, - }, - "allOf": [ - { - "if": { - "properties": {"type": {"enum": ["discount"]}}, - "required": ["type"], - }, - "then": {"properties": {"amount": {"exclusiveMaximum": 0}}}, - }, - { - "if": { - "properties": {"type": {"enum": ["tax"]}}, - "required": ["type"], - }, - "then": {"properties": {"amount": {"minimum": 0}}}, - }, - ], - } - - def test_schema_scan_reads_rules_carried_as_allof_branches(self): - """An if/then branch constrains the enclosing object's properties.""" - with tempfile.TemporaryDirectory() as tmp: - Path(tmp, "total.json").write_text(json.dumps(self._schema())) - found = postprocess_models.find_conditional_bounds(Path(tmp)) - self.assertEqual(found, {"Total": self.RULES}) - - def test_schema_scan_skips_rules_whose_fields_were_stripped(self): - """A request variant drops the fields, so the rule cannot apply.""" - schema = self._schema() - schema["properties"] = {} - with tempfile.TemporaryDirectory() as tmp: - Path(tmp, "total_create_request.json").write_text( - json.dumps(schema) - ) - stderr = io.StringIO() - with contextlib.redirect_stderr(stderr): - found = postprocess_models.find_conditional_bounds(Path(tmp)) - self.assertEqual(found, {}) - self.assertNotIn("unsupported", stderr.getvalue()) - - def test_schema_scan_warns_on_unsupported_shape(self): - schema = self._schema() - schema["allOf"][0]["then"]["properties"]["amount"] = {"pattern": "^x$"} - with tempfile.TemporaryDirectory() as tmp: - Path(tmp, "total.json").write_text(json.dumps(schema)) - stderr = io.StringIO() - with contextlib.redirect_stderr(stderr): - found = postprocess_models.find_conditional_bounds(Path(tmp)) - # The malformed branch is dropped; the well-formed sibling survives. - self.assertEqual(found, {"Total": [self.RULES[1]]}) - self.assertIn("unsupported", stderr.getvalue()) - - def test_schema_scan_recognizes_const_pinning(self): - """A `then.properties..const` pin is dropped today: describe() - only accepts the four numeric bound keywords in _BOUND_KEYWORDS, so - `set(constraint) - set(_BOUND_KEYWORDS)` is non-empty for a bare - `{"const": ...}` constraint and the whole rule returns None. This - mirrors unit.json exactly: when unit is C62, scale must be exactly - 0. The branch carries no title of its own (unlike the JWK case - below), isolating bug (b) from bug (c). - """ - schema = { - "title": "Unit", - "type": "object", - "properties": { - "unit": {"type": "string"}, - "scale": {"type": "integer"}, - }, - "allOf": [ - { - "if": { - "properties": {"unit": {"const": "C62"}}, - "required": ["unit"], - }, - "then": {"properties": {"scale": {"const": 0}}}, - } - ], - } - with tempfile.TemporaryDirectory() as tmp: - Path(tmp, "unit.json").write_text(json.dumps(schema)) - found = postprocess_models.find_conditional_bounds(Path(tmp)) - self.assertEqual( - found, - { - "Unit": [ - { - "discriminator": "unit", - "values": ["C62"], - "bounds": {"scale": {"const": 0}}, - } - ] - }, - ) - - def test_schema_scan_recognizes_const_pinning_in_titled_allof_branch( - self, - ): - """profile.json's JWK curve/algorithm pairing rules combine both - gaps at once: `then.properties.alg.const` (bug b, see above) inside - a branch that carries its own documentation `title` (bug c, see - ConditionalRequiredInjectorTest) which must not overwrite the - enclosing `JwkPublicKey` class name. - """ - schema = { - "$defs": { - "jwk_public_key": { - "type": "object", - "required": ["kid", "kty"], - "properties": { - "kid": {"type": "string"}, - "kty": {"type": "string"}, - "crv": {"type": "string"}, - "alg": {"type": "string"}, - }, - "allOf": [ - { - "title": "P-256 pairs with ES256", - "if": { - "properties": {"crv": {"const": "P-256"}}, - "required": ["crv"], - }, - "then": {"properties": {"alg": {"const": "ES256"}}}, - } - ], - } - } - } - with tempfile.TemporaryDirectory() as tmp: - Path(tmp, "profile.json").write_text(json.dumps(schema)) - found = postprocess_models.find_conditional_bounds(Path(tmp)) - self.assertEqual( - found, - { - "JwkPublicKey": [ - { - "discriminator": "crv", - "values": ["P-256"], - "bounds": {"alg": {"const": "ES256"}}, - } - ] - }, - ) - - def test_injection_is_idempotent(self): - once = postprocess_models.inject_conditional_bounds( - self.MODULE, "Total", self.RULES - ) - twice = postprocess_models.inject_conditional_bounds( - once, "Total", self.RULES - ) - self.assertEqual(once, twice) - - @unittest.skipUnless(HAVE_SDK, "executing the module needs pydantic") - def test_injected_validator_enforces_const_pinning(self): - module = ( - "from __future__ import annotations\n" - "\n" - "from pydantic import BaseModel, ConfigDict\n" - "\n" - "\n" - "class Unit(BaseModel):\n" - ' model_config = ConfigDict(extra="allow")\n' - " unit: str\n" - " scale: int | None = 0\n" - ) - rules = [ - { - "discriminator": "unit", - "values": ["C62"], - "bounds": {"scale": {"const": 0}}, - } - ] - out = postprocess_models.inject_conditional_bounds( - module, "Unit", rules - ) - namespace: dict = {} - exec(compile(out, "", "exec"), namespace) # noqa: S102 - unit = namespace["Unit"] - with self.assertRaises(ValidationError): - unit(unit="C62", scale=5) - unit(unit="C62", scale=0) - unit(unit="C62") - # A unit outside the pinned vocabulary is unconstrained. - unit(unit="KGM", scale=3) - - @unittest.skipUnless(HAVE_SDK, "executing the module needs pydantic") - def test_injected_validator_enforces_conditional_bounds(self): - out = postprocess_models.inject_conditional_bounds( - self.MODULE, "Total", self.RULES - ) - namespace: dict = {} - exec(compile(out, "", "exec"), namespace) # noqa: S102 - total = namespace["Total"] - with self.assertRaises(ValidationError): - total(type="discount", amount=500) - with self.assertRaises(ValidationError): - total(type="tax", amount=-1) - total(type="discount", amount=-500) - total(type="tax", amount=0) - # A type carrying no rule is unconstrained (the vocabulary is open). - total(type="total", amount=-5) - - -class ConditionalArrayRetypingInjectorTest(unittest.TestCase): - """A discriminator retyping an array property's items to a different - referenced schema file is a third if/then shape, distinct from - conditional-required and conditional-bounds above. This mirrors - fulfillment_method.json: the base `destinations` property is typed via - `items.$ref` to fulfillment_destination.json, but a `shipping` method's - destinations should really be shipping_destination.json items (postal - address fields, `type` const `shipping_address`) and a `pickup` - method's should really be location_destination.json items (`type` - const `business_location`). The generator drops both retyping branches - entirely -- no scanner in this module ever looked for this shape. - """ - - MODULE = ( - "from __future__ import annotations\n" - "\n" - "from pydantic import BaseModel, ConfigDict\n" - "\n" - "from . import destination\n" - "\n" - "\n" - "class Method(BaseModel):\n" - ' model_config = ConfigDict(extra="allow")\n' - " type: str\n" - " destinations: list[destination.Destination] | None = None\n" - ) - RULES = [ - { - "discriminator": "type", - "values": ["shipping"], - "field": "destinations", - "required": ["id", "type"], - "consts": {"type": "shipping_address"}, - }, - { - "discriminator": "type", - "values": ["pickup"], - "field": "destinations", - "required": ["type"], - "consts": {"type": "business_location"}, - }, - ] - - def _schema(self): - return { - "title": "Method", - "type": "object", - "properties": { - "type": {"type": "string"}, - "destinations": { - "type": "array", - "items": {"$ref": "destination.json"}, - }, - }, - "allOf": [ - { - "if": { - "properties": {"type": {"const": "shipping"}}, - "required": ["type"], - }, - "then": { - "properties": { - "destinations": { - "type": "array", - "items": {"$ref": "shipping_destination.json"}, - } - } - }, - }, - { - "if": { - "properties": {"type": {"const": "pickup"}}, - "required": ["type"], - }, - "then": { - "properties": { - "destinations": { - "type": "array", - "items": {"$ref": "location_destination.json"}, - } - } - }, - }, - ], - } - - def _write_schema_tree(self, tmp): - Path(tmp, "method.json").write_text(json.dumps(self._schema())) - Path(tmp, "destination.json").write_text( - json.dumps( - { - "title": "Destination", - "type": "object", - "required": ["id", "type"], - "properties": { - "id": {"type": "string"}, - "type": {"type": "string"}, - }, - } - ) - ) - Path(tmp, "shipping_destination.json").write_text( - json.dumps( - { - "title": "Shipping Destination", - "type": "object", - "required": ["id", "type"], - "properties": { - "id": {"type": "string"}, - "type": {"type": "string", "const": "shipping_address"}, - }, - "allOf": [{"$ref": "postal_address.json"}], - } - ) - ) - Path(tmp, "location_destination.json").write_text( - json.dumps( - { - "title": "Business Location Destination", - "type": "object", - "required": ["type"], - "properties": { - "type": {"type": "string", "const": "business_location"} - }, - "allOf": [{"$ref": "location_summary.json"}], - } - ) - ) - - def test_schema_scan_reads_both_retyping_branches(self): - with tempfile.TemporaryDirectory() as tmp: - self._write_schema_tree(tmp) - found = postprocess_models.find_conditional_array_retyping( - Path(tmp) - ) - self.assertEqual(found, {"Method": self.RULES}) - - def test_schema_scan_ignores_branch_matching_the_base_ref(self): - # A then.properties..items.$ref identical to the base ref is - # not a retype -- nothing to approximate. The sibling pickup branch - # (still a genuine retype) is unaffected. - schema = self._schema() - schema["allOf"][0]["then"]["properties"]["destinations"]["items"][ - "$ref" - ] = "destination.json" - with tempfile.TemporaryDirectory() as tmp: - self._write_schema_tree(tmp) - Path(tmp, "method.json").write_text(json.dumps(schema)) - found = postprocess_models.find_conditional_array_retyping( - Path(tmp) - ) - self.assertEqual(found, {"Method": [self.RULES[1]]}) - - def test_schema_scan_skips_rule_whose_field_was_stripped(self): - # A request variant that omits `destinations` entirely (as - # fulfillment_method_create_request.json does) makes the rule - # inapplicable, not malformed -- no warning, and no rule recorded - # for the variant's own class. - schema = self._schema() - schema["title"] = "Method Create Request" - del schema["properties"]["destinations"] - with tempfile.TemporaryDirectory() as tmp: - self._write_schema_tree(tmp) - Path(tmp, "method_create_request.json").write_text( - json.dumps(schema) - ) - stderr = io.StringIO() - with contextlib.redirect_stderr(stderr): - found = postprocess_models.find_conditional_array_retyping( - Path(tmp) - ) - self.assertNotIn("MethodCreateRequest", found) - self.assertEqual(found, {"Method": self.RULES}) - self.assertNotIn("unsupported", stderr.getvalue()) - - def test_schema_scan_warns_when_retyped_ref_cannot_be_loaded(self): - schema = self._schema() - with tempfile.TemporaryDirectory() as tmp: - Path(tmp, "method.json").write_text(json.dumps(schema)) - Path(tmp, "destination.json").write_text( - json.dumps({"title": "Destination", "type": "object"}) - ) - # shipping_destination.json / location_destination.json are - # deliberately absent. - stderr = io.StringIO() - with contextlib.redirect_stderr(stderr): - found = postprocess_models.find_conditional_array_retyping( - Path(tmp) - ) - self.assertEqual(found, {}) - self.assertIn("could not be loaded", stderr.getvalue()) - - def test_injection_is_idempotent(self): - once = postprocess_models.inject_conditional_array_retyping( - self.MODULE, "Method", self.RULES - ) - twice = postprocess_models.inject_conditional_array_retyping( - once, "Method", self.RULES - ) - self.assertEqual(once, twice) - - @unittest.skipUnless(HAVE_SDK, "executing the module needs pydantic") - def test_injected_validator_enforces_retyping(self): - module = ( - "from __future__ import annotations\n" - "\n" - "from pydantic import BaseModel, ConfigDict\n" - "\n" - "\n" - "class Destination(BaseModel):\n" - ' model_config = ConfigDict(extra="allow")\n' - " type: str\n" - " id: str\n" - "\n" - "\n" - "class Method(BaseModel):\n" - ' model_config = ConfigDict(extra="allow")\n' - " type: str\n" - " destinations: list[Destination] | None = None\n" - ) - out = postprocess_models.inject_conditional_array_retyping( - module, "Method", self.RULES - ) - namespace: dict = {} - exec(compile(out, "", "exec"), namespace) # noqa: S102 - # Forward reference (from __future__ import annotations): Method's - # "destinations: list[Destination]" annotation resolves once both - # classes exist in the exec'd namespace. - namespace["Method"].model_rebuild(_types_namespace=namespace) - method_cls = namespace["Method"] - destination_cls = namespace["Destination"] - with self.assertRaises(ValidationError): - method_cls( - type="shipping", - destinations=[ - destination_cls(type="business_location", id="d1") - ], - ) - with self.assertRaises(ValidationError): - method_cls( - type="pickup", - destinations=[ - destination_cls(type="shipping_address", id="d1") - ], - ) - method_cls( - type="shipping", - destinations=[destination_cls(type="shipping_address", id="d1")], - ) - method_cls( - type="pickup", - destinations=[destination_cls(type="business_location", id="d1")], - ) - # A type carrying no rule is unconstrained (open vocabulary). - method_cls( - type="courier", - destinations=[destination_cls(type="anything", id="d1")], - ) - # No destinations at all is unconstrained regardless of type. - method_cls(type="shipping") - - -class DependentRequiredInjectorTest(unittest.TestCase): - """The dependentRequired post-generation injector's behavior.""" - - SCHEMA = { - "title": "Time Interval", - "type": "object", - "properties": { - "opens": {"type": "string"}, - "closes": {"type": "string"}, - }, - "dependentRequired": { - "opens": ["closes"], - "closes": ["opens"], - }, - } - - MODULE = ( - "from __future__ import annotations\n" - "\n" - "from pydantic import BaseModel, ConfigDict\n" - "\n" - "\n" - "class TimeInterval(BaseModel):\n" - ' model_config = ConfigDict(extra="allow")\n' - " opens: str | None = None\n" - " closes: str | None = None\n" - ) - - def test_schema_scan_finds_root_rules(self): - with tempfile.TemporaryDirectory() as tmp: - Path(tmp, "time_interval.json").write_text( - json.dumps(self.SCHEMA), encoding="utf-8" - ) - found = postprocess_models.find_root_dependent_required(Path(tmp)) - self.assertEqual( - found, - { - "TimeInterval": { - "opens": ["closes"], - "closes": ["opens"], - } - }, - ) - - def test_schema_scan_filters_rules_outside_declared_properties(self): - schema = copy.deepcopy(self.SCHEMA) - schema["dependentRequired"]["opens"] = ["timezone"] - with tempfile.TemporaryDirectory() as tmp: - Path(tmp, "time_interval.json").write_text( - json.dumps(schema), encoding="utf-8" - ) - found = postprocess_models.find_root_dependent_required(Path(tmp)) - self.assertEqual(found, {"TimeInterval": {"closes": ["opens"]}}) - - def test_injection_is_idempotent(self): - rules = {"opens": ["closes"], "closes": ["opens"]} - once = postprocess_models.inject_dependent_required( - self.MODULE, "TimeInterval", rules - ) - twice = postprocess_models.inject_dependent_required( - once, "TimeInterval", rules - ) - self.assertEqual(once, twice) - - def test_injection_skips_rules_for_projected_out_fields(self): - projected = self.MODULE.replace(" closes: str | None = None\n", "") - out = postprocess_models.inject_dependent_required( - projected, - "TimeInterval", - {"opens": ["closes"], "closes": ["opens"]}, - ) - self.assertEqual(out, projected) - - @unittest.skipUnless(HAVE_SDK, "executing the module needs pydantic") - def test_injected_validator_uses_property_presence(self): - rules = {"opens": ["closes"], "closes": ["opens"]} - out = postprocess_models.inject_dependent_required( - self.MODULE, "TimeInterval", rules - ) - namespace: dict = {} - exec(compile(out, "", "exec"), namespace) # noqa: S102 - interval = namespace["TimeInterval"] - - interval() - interval(opens="09:00", closes="17:00") - interval(opens=None, closes=None) - with self.assertRaisesRegex(ValidationError, "dependentRequired"): - interval(opens="09:00") - with self.assertRaisesRegex(ValidationError, "dependentRequired"): - interval(closes="17:00") - with self.assertRaisesRegex(ValidationError, "dependentRequired"): - interval(opens=None) - - -class InjectorTest(unittest.TestCase): - """The post-generation injector's own behavior.""" - - SCHEMA = { - "title": "Sample", - "type": "object", - "minProperties": 2, - "properties": {"a": {"type": "string"}, "b": {"type": "string"}}, - } - - MODULE = ( - "from __future__ import annotations\n" - "\n" - "from pydantic import BaseModel, ConfigDict\n" - "\n" - "\n" - "class Sample(BaseModel):\n" - ' """A sample."""\n' - "\n" - " model_config = ConfigDict(\n" - ' extra="allow",\n' - " )\n" - " a: str | None = None\n" - " b: str | None = None\n" - ) - - def test_injects_validator_with_declared_minimum(self): - out = postprocess_models.inject_min_properties(self.MODULE, "Sample", 2) - self.assertIn("model_validator", out) - self.assertIn("at least 2", out.lower()) - - @unittest.skipUnless(HAVE_SDK, "executing the module needs pydantic") - def test_injected_validator_enforces_count(self): - out = postprocess_models.inject_min_properties(self.MODULE, "Sample", 2) - namespace: dict = {} - exec(compile(out, "", "exec"), namespace) # noqa: S102 - sample_cls = namespace["Sample"] - with self.assertRaises(ValidationError): - sample_cls(a="only-one") - sample_cls(a="one", b="two") - - def test_injection_is_idempotent(self): - once = postprocess_models.inject_min_properties( - self.MODULE, "Sample", 2 - ) - twice = postprocess_models.inject_min_properties(once, "Sample", 2) - self.assertEqual(once, twice) - - def test_schema_scan_finds_root_constraints(self): - with tempfile.TemporaryDirectory() as tmp: - sub = Path(tmp) / "sub" - sub.mkdir() - (sub / "sample.json").write_text(json.dumps(self.SCHEMA)) - (sub / "plain.json").write_text( - json.dumps( - {"title": "Plain", "type": "object", "properties": {}} - ) - ) - found = postprocess_models.find_root_min_properties(Path(tmp)) - self.assertEqual(found, {"Sample": 2}) - - -class MaxPropertiesInjectorTest(unittest.TestCase): - """maxProperties is the symmetric twin of minProperties (see #49/#55), - but only minProperties was ever scanned: find_root_min_properties reads - schema.get("minProperties") and there is no find_root_max_properties at - all, so location_serves.json's maxProperties: 1 -- "the Platform MUST - supply exactly one target form" -- is silently dropped. This mirrors - InjectorTest above one for one, for the max side. - """ - - SCHEMA = { - "title": "Sample", - "type": "object", - "maxProperties": 1, - "properties": {"a": {"type": "string"}, "b": {"type": "string"}}, - } - - MODULE = ( - "from __future__ import annotations\n" - "\n" - "from pydantic import BaseModel, ConfigDict\n" - "\n" - "\n" - "class Sample(BaseModel):\n" - ' """A sample."""\n' - "\n" - " model_config = ConfigDict(\n" - ' extra="allow",\n' - " )\n" - " a: str | None = None\n" - " b: str | None = None\n" - ) - - def test_injects_validator_with_declared_maximum(self): - out = postprocess_models.inject_max_properties(self.MODULE, "Sample", 1) - self.assertIn("model_validator", out) - self.assertIn("at most 1", out.lower()) - - @unittest.skipUnless(HAVE_SDK, "executing the module needs pydantic") - def test_injected_validator_enforces_count(self): - out = postprocess_models.inject_max_properties(self.MODULE, "Sample", 1) - namespace: dict = {} - exec(compile(out, "", "exec"), namespace) # noqa: S102 - sample_cls = namespace["Sample"] - with self.assertRaises(ValidationError): - sample_cls(a="one", b="two") - sample_cls(a="only-one") - sample_cls() - - def test_injection_is_idempotent(self): - once = postprocess_models.inject_max_properties( - self.MODULE, "Sample", 1 - ) - twice = postprocess_models.inject_max_properties(once, "Sample", 1) - self.assertEqual(once, twice) - - def test_schema_scan_finds_root_constraints(self): - with tempfile.TemporaryDirectory() as tmp: - sub = Path(tmp) / "sub" - sub.mkdir() - (sub / "sample.json").write_text(json.dumps(self.SCHEMA)) - (sub / "plain.json").write_text( - json.dumps( - {"title": "Plain", "type": "object", "properties": {}} - ) - ) - found = postprocess_models.find_root_max_properties(Path(tmp)) - self.assertEqual(found, {"Sample": 1}) - - def test_schema_scan_ignores_object_without_declared_properties(self): - # Mirrors find_root_min_properties: maxProperties on a free-form - # object property (no named properties) is already handled natively - # by the generator (Field(max_length=...) on the dict field), so a - # bare maxProperties with no properties is out of scope here. - schema = { - "title": "OpenMap", - "type": "object", - "maxProperties": 3, - } - with tempfile.TemporaryDirectory() as tmp: - Path(tmp, "open_map.json").write_text(json.dumps(schema)) - found = postprocess_models.find_root_max_properties(Path(tmp)) - self.assertEqual(found, {}) - - def test_both_bounds_coexist_on_the_same_class(self): - """location_serves.json declares both minProperties: 1 AND - maxProperties: 1 on the same object; both validators must be - injectable into the same class without clobbering each other.""" - module = postprocess_models.inject_min_properties( - self.MODULE, "Sample", 1 - ) - module = postprocess_models.inject_max_properties(module, "Sample", 1) - self.assertIn("_enforce_min_properties", module) - self.assertIn("_enforce_max_properties", module) - if HAVE_SDK: - namespace: dict = {} - exec(compile(module, "", "exec"), namespace) # noqa: S102 - sample_cls = namespace["Sample"] - with self.assertRaises(ValidationError): - sample_cls() - with self.assertRaises(ValidationError): - sample_cls(a="one", b="two") - sample_cls(a="only-one") - - -@unittest.skipUnless( - HAVE_SDK, "requires the installed package (pip install -e .)" -) -class LocationServesMaxPropertiesSemanticTest(unittest.TestCase): - """location_serves.json: "The Platform MUST supply exactly one target - form" -- minProperties: 1 AND maxProperties: 1 together. Only the - minimum was ever enforced (see MaxPropertiesInjectorTest above), so a - map naming both point and address currently validates in violation of - the schema. - """ - - def _location_serves(self): - from ucp_sdk.models.schemas.common.types.location_serves import ( - LocationServes, - ) - - return LocationServes - - def _geo(self): - from ucp_sdk.models.schemas.common.types.geo import Geo - - return Geo - - def _address(self): - from ucp_sdk.models.schemas.common.types.location_serves import ( - Address, - ) - - return Address - - def test_both_point_and_address_rejected(self): - with self.assertRaises(ValidationError): - self._location_serves()( - point=self._geo()(latitude=1.0, longitude=2.0), - address=self._address()(address_country="US"), - ) - - def test_point_only_accepted(self): - location = self._location_serves()( - point=self._geo()(latitude=1.0, longitude=2.0) - ) - self.assertIsNotNone(location.point) - - def test_address_only_accepted(self): - location = self._location_serves()( - address=self._address()(address_country="US") - ) - self.assertIsNotNone(location.address) - - def test_empty_still_rejected_by_the_existing_minimum(self): - # Unaffected by this fix; confirms minProperties: 1 still holds. - with self.assertRaises(ValidationError): - self._location_serves()() - - def test_extension_key_alongside_point_rejected(self): - # extra="allow": an extension form key still counts toward the - # maxProperties=1 total per JSON Schema's key-counting semantics. - with self.assertRaises(ValidationError): - self._location_serves().model_validate( - { - "point": {"latitude": 1.0, "longitude": 2.0}, - "dev.example.custom_target": {"foo": "bar"}, - } - ) - - -@unittest.skipUnless( - HAVE_SDK, "requires the installed package (pip install -e .)" -) -class TotalsContainsTest(unittest.TestCase): - """totals.json requires exactly one ``subtotal`` AND one ``total`` entry. - - Both rules live as two ``allOf`` ``contains`` branches; the generator drops - them, leaving ``Totals`` a bare ``list[Total]``. The post-generation injector - reads the pristine schema and restores BOTH bounds as an ``AfterValidator`` - on the alias — the same check reaching the generated request variants too. - """ - - SUBTOTAL = {"type": "subtotal", "amount": 100, "display_text": "Subtotal"} - TOTAL = {"type": "total", "amount": 100, "display_text": "Total"} - - #: (name, array, expected-valid?) exercised against every totals model. - def _cases(self): - return [ - ("empty", [], False), - ("two_total_no_subtotal", [self.TOTAL, self.TOTAL], False), - ("two_subtotal_no_total", [self.SUBTOTAL, self.SUBTOTAL], False), - ("subtotal_only", [self.SUBTOTAL], False), - ("total_only", [self.TOTAL], False), - ("valid_subtotal_and_total", [self.SUBTOTAL, self.TOTAL], True), - ] - - def _assert_matrix(self, alias): - adapter = TypeAdapter(alias) - for name, array, valid in self._cases(): - with self.subTest(model=alias.__name__, case=name): - if valid: - self.assertEqual(len(adapter.validate_python(array)), 2) - else: - with self.assertRaises(ValidationError): - adapter.validate_python(array) - - def test_base_totals_enforces_both_bounds(self): - self._assert_matrix(Totals) - - def test_create_request_variant_enforces_both_bounds(self): - self._assert_matrix(TotalsCreateRequest) - - def test_update_request_variant_enforces_both_bounds(self): - self._assert_matrix(TotalsUpdateRequest) - - def test_custom_type_requires_display_text(self): - base = [self.SUBTOTAL, self.TOTAL] - for alias in (Totals, TotalsCreateRequest, TotalsUpdateRequest): - adapter = TypeAdapter(alias) - with self.subTest(model=alias.__name__): - with self.assertRaisesRegex(ValidationError, "display_text"): - adapter.validate_python( - base + [{"type": "surcharge", "amount": 5}] - ) - adapter.validate_python(base + [{"type": "tax", "amount": 5}]) - adapter.validate_python( - base - + [ - { - "type": "surcharge", - "amount": 5, - "display_text": "Surcharge", - } - ] - ) - - def test_missing_total_names_the_total_rule(self): - # A subtotal-only array must fail specifically on the total rule. - with self.assertRaisesRegex(ValidationError, "total"): - TypeAdapter(Totals).validate_python([self.SUBTOTAL]) - - -class ArrayContainsInjectorTest(unittest.TestCase): - """The array-contains injector's own behavior.""" - - MODULE = ( - "from __future__ import annotations\n" - "\n" - "from typing import Annotated\n" - "\n" - "from pydantic import BaseModel, ConfigDict, Field\n" - "from typing_extensions import TypeAliasType\n" - "\n" - "\n" - "class Total(BaseModel):\n" - ' model_config = ConfigDict(extra="allow")\n' - " type: str\n" - " amount: int\n" - "\n" - "\n" - "Totals = TypeAliasType(\n" - ' "Totals", Annotated[list[Total], Field(..., title="Totals")]\n' - ")\n" - ) - - #: The same alias, but formatted the way ruff/black renders it once the - #: item type name is long enough to force line-wrapping (e.g. once a - #: request-variant $ref like ``total_create_request.TotalCreateRequest`` - #: replaces the short ``Total`` reference). The trailing comma after - #: ``Field(...)`` before the closing ``]`` is the shape that matters here. - MODULE_LINE_WRAPPED = ( - "from __future__ import annotations\n" - "\n" - "from typing import Annotated\n" - "\n" - "from pydantic import Field\n" - "from typing_extensions import TypeAliasType\n" - "\n" - "from . import total_create_request\n" - "\n" - "\n" - "TotalsCreateRequest = TypeAliasType(\n" - ' "TotalsCreateRequest",\n' - " Annotated[\n" - " list[total_create_request.TotalCreateRequest],\n" - ' Field(..., title="Totals Create Request"),\n' - " ],\n" - ")\n" - ) - - #: subtotal AND total, mirroring the real totals.json. - GROUPS = [ - {"pairs": [("type", "subtotal")], "min": 1, "max": 1}, - {"pairs": [("type", "total")], "min": 1, "max": 1}, - ] - - ITEM_CONDITION = { - "field": "type", - "excluded": ["subtotal", "total"], - "required": ["display_text"], - } - - def test_scan_reads_both_contains_from_allof_branches(self): - # The pristine totals.json shape: two allOf contains branches. - schema = { - "title": "Totals", - "type": "array", - "items": { - "allOf": [ - { - "if": { - "properties": { - "type": {"not": {"enum": ["subtotal", "total"]}} - }, - "required": ["type"], - }, - "then": {"required": ["display_text"]}, - } - ] - }, - "allOf": [ - { - "contains": {"properties": {"type": {"const": "subtotal"}}}, - "minContains": 1, - "maxContains": 1, - }, - { - "contains": {"properties": {"type": {"const": "total"}}}, - "minContains": 1, - "maxContains": 1, - }, - ], - } - with tempfile.TemporaryDirectory() as tmp: - (Path(tmp) / "totals.json").write_text(json.dumps(schema)) - found = postprocess_models.find_array_contains_constraints( - Path(tmp) - ) - self.assertEqual(set(found), {"totals"}) - self.assertEqual(found["totals"]["title"], "Totals") - self.assertEqual( - [g["pairs"] for g in found["totals"]["groups"]], - [[("type", "subtotal")], [("type", "total")]], - ) - self.assertEqual( - found["totals"]["item_condition"], - self.ITEM_CONDITION, - ) - - def test_scan_reads_root_level_single_contains(self): - # A root-level (non-allOf) contains still yields one group. - schema = { - "title": "Totals", - "type": "array", - "items": {"type": "object"}, - "contains": {"properties": {"type": {"const": "subtotal"}}}, - "minContains": 1, - "maxContains": 1, - } - with tempfile.TemporaryDirectory() as tmp: - (Path(tmp) / "totals.json").write_text(json.dumps(schema)) - found = postprocess_models.find_array_contains_constraints( - Path(tmp) - ) - self.assertEqual( - found["totals"]["groups"], - [{"pairs": [("type", "subtotal")], "min": 1, "max": 1}], - ) - - def test_scan_ignores_non_array_and_predicateless_contains(self): - with tempfile.TemporaryDirectory() as tmp: - # An object schema (not an array) is out of scope. - (Path(tmp) / "obj.json").write_text( - json.dumps({"title": "Obj", "type": "object"}) - ) - # A contains with no derivable const predicate is skipped. - (Path(tmp) / "arr.json").write_text( - json.dumps( - { - "title": "Arr", - "type": "array", - "contains": {"required": ["type"]}, - } - ) - ) - with contextlib.redirect_stderr(io.StringIO()): - found = postprocess_models.find_array_contains_constraints( - Path(tmp) - ) - self.assertEqual(found, {}) - - def test_injects_after_validator_and_import(self): - out = postprocess_models.inject_array_contains( - self.MODULE, "Totals", self.GROUPS - ) - self.assertIn("AfterValidator(_enforce_contains_totals)", out) - self.assertRegex(out, r"from pydantic import .*AfterValidator") - # Both predicates are present in the injected function (pre-format - # output uses repr() single quotes; ruff restyles them later). - self.assertIn("== 'subtotal'", out) - self.assertIn("== 'total'", out) - - def test_injection_is_idempotent(self): - once = postprocess_models.inject_array_contains( - self.MODULE, "Totals", self.GROUPS - ) - twice = postprocess_models.inject_array_contains( - once, "Totals", self.GROUPS - ) - self.assertEqual(once, twice) - - def test_injects_cleanly_when_annotated_is_line_wrapped(self): - """A line-wrapped Annotated[...] with a trailing comma before the - closing bracket must still parse (see #34/#35: a longer item-type - reference, such as a request-variant $ref, pushes the formatter to - wrap the annotation onto multiple lines with a trailing comma; a - naive "insert before the closing bracket" splice then lands after - that comma and produces "Field(...),\\n, AfterValidator(...)]" - - two commas with nothing between them, a SyntaxError). - """ - out = postprocess_models.inject_array_contains( - self.MODULE_LINE_WRAPPED, "TotalsCreateRequest", self.GROUPS - ) - - # The regression: this must be syntactically valid Python. - ast.parse(out) - - self.assertIn( - "AfterValidator(_enforce_contains_totals_create_request)", out - ) - # No orphaned comma left behind by the splice. - self.assertNotRegex(out, r",\s*,") - - @unittest.skipUnless(HAVE_SDK, "executing the module needs pydantic") - def test_injected_validator_enforces_both_bounds(self): - out = postprocess_models.inject_array_contains( - self.MODULE, "Totals", self.GROUPS - ) - namespace: dict = {} - exec(compile(out, "", "exec"), namespace) # noqa: S102 - adapter = TypeAdapter(namespace["Totals"]) - sub = {"type": "subtotal", "amount": 1} - tot = {"type": "total", "amount": 1} - for bad in ([], [sub], [tot], [sub, sub], [tot, tot]): - with self.assertRaises(ValidationError): - adapter.validate_python(bad) - adapter.validate_python([sub, tot]) - - @unittest.skipUnless(HAVE_SDK, "executing the module needs pydantic") - def test_injected_validator_requires_custom_display_text(self): - out = postprocess_models.inject_array_contains( - self.MODULE, "Totals", self.GROUPS, self.ITEM_CONDITION - ) - namespace: dict = {} - exec(compile(out, "", "exec"), namespace) # noqa: S102 - adapter = TypeAdapter(namespace["Totals"]) - base = [ - {"type": "subtotal", "amount": 1}, - {"type": "total", "amount": 1}, - ] - with self.assertRaisesRegex(ValidationError, "display_text"): - adapter.validate_python(base + [{"type": "surcharge", "amount": 1}]) - adapter.validate_python( - base - + [ - { - "type": "surcharge", - "amount": 1, - "display_text": "Surcharge", - } - ] - ) - - -class UniqueItemsInjectorTest(unittest.TestCase): - """The uniqueItems post-generation injector's own behavior.""" - - SCHEMA_TREE = { - "title": "First", - "properties": { - "tags": { - "type": "array", - "items": {"type": "string"}, - "uniqueItems": True, - }, - "label": {"type": "array", "items": {"type": "string"}}, - "name": {"type": "string"}, - "nested": { - "type": "object", - "properties": { - "codes": { - "type": "array", - "items": {"type": "string"}, - "uniqueItems": True, - } - }, - }, - }, - } - - MODULE = ( - "from __future__ import annotations\n" - "\n" - "from pydantic import BaseModel, ConfigDict\n" - "\n" - "\n" - "class First(BaseModel):\n" - ' """First."""\n' - "\n" - " model_config = ConfigDict(\n" - ' extra="allow",\n' - " )\n" - " tags: list[str] | None = None\n" - " name: str | None = None\n" - "\n" - "\n" - "class Second(BaseModel):\n" - ' """Second."""\n' - "\n" - " model_config = ConfigDict(\n" - ' extra="allow",\n' - " )\n" - " tags: list[str] | None = None\n" - " count: list[int] | None = None\n" - ) - - def test_find_unique_items_fields_walks_nested_properties(self) -> None: - """Root and nested array props with uniqueItems are collected.""" - with tempfile.TemporaryDirectory() as tmp: - (Path(tmp) / "schema.json").write_text(json.dumps(self.SCHEMA_TREE)) - fields = postprocess_models.find_unique_items_fields(Path(tmp)) - self.assertEqual(fields, {"First": {"tags"}, "Nested": {"codes"}}) - - def test_find_unique_items_fields_ignores_false_and_non_arrays( - self, - ) -> None: - """uniqueItems: false and non-array props do not qualify.""" - schema = { - "properties": { - "a": { - "type": "array", - "items": {"type": "string"}, - "uniqueItems": False, - }, - "b": {"type": "string", "uniqueItems": True}, - } - } - with tempfile.TemporaryDirectory() as tmp: - (Path(tmp) / "s.json").write_text(json.dumps(schema)) - fields = postprocess_models.find_unique_items_fields(Path(tmp)) - self.assertEqual(fields, {}) - - def test_inject_targets_matching_list_fields_only(self) -> None: - """Only the declaring class's matching list field gets a validator.""" - out = postprocess_models.inject_unique_items( - self.MODULE, {"First": {"tags"}} - ) - self.assertIn("field_validator", out) - self.assertIn("_enforce_unique_items_tags", out) - self.assertEqual(out.count("def _enforce_unique_items_tags("), 1) - self.assertNotIn("_enforce_unique_items_name", out) - self.assertNotIn("_enforce_unique_items_count", out) - - def test_inject_no_match_leaves_source_unchanged(self) -> None: - """No matching list field means the module is untouched.""" - self.assertEqual( - postprocess_models.inject_unique_items( - self.MODULE, {"First": {"missing"}} - ), - self.MODULE, - ) - - def test_injection_is_idempotent(self) -> None: - """Re-running the injector changes nothing.""" - unique_fields = {"First": {"tags"}} - once = postprocess_models.inject_unique_items( - self.MODULE, unique_fields - ) - twice = postprocess_models.inject_unique_items(once, unique_fields) - self.assertEqual(once, twice) - - @unittest.skipUnless(HAVE_SDK, "executing the module needs pydantic") - def test_injected_validator_rejects_duplicates(self) -> None: - """The injected field_validator enforces uniqueness at runtime.""" - out = postprocess_models.inject_unique_items( - self.MODULE, {"First": {"tags"}} - ) - namespace: dict = {} - exec(compile(out, "", "exec"), namespace) # noqa: S102 - first = namespace["First"] - first(tags=["a", "b"]) # unique passes - first() # None passes - with self.assertRaises(ValidationError): - first(tags=["a", "a"]) # duplicate rejected - - -@unittest.skipUnless( - HAVE_SDK, "requires the installed package (pip install -e .)" -) -class UniqueItemsSemanticTest(unittest.TestCase): - """Committed models enforce uniqueItems on declared array fields.""" - - # NOTE(root-cause-0): card_payment_instrument.json no longer declares a - # Constraints.brands field (uniqueItems) as of the pinned 2026-08-25 UCP - # schema -- the module now generates only Display, ConstraintTarget and - # CardPaymentInstrument (verified against - # src/ucp_sdk/models/schemas/common/types/card_payment_instrument.py). - # These two tests exercised a schema shape that no longer exists; the - # HAVE_SDK import gate bug (see the top of this file) had been hiding - # that they could not pass, not just that they were unrelated to SDK - # availability. Documented skip rather than silent deletion: the - # uniqueItems mechanism itself stays covered by UniqueItemsInjectorTest - # (injector unit tests) and by other committed models with uniqueItems - # fields (e.g. common.types.constraint_expression, context, - # location_filter, request_constraints). - @unittest.skip( - "card_payment_instrument.Constraints.brands (uniqueItems) was " - "removed from the schema before the pinned 2026-08-25 UCP release; " - "no current committed model at this path carries a brands field" - ) - def test_brands_rejects_duplicates(self) -> None: - """card_payment_instrument brands rejects duplicate entries.""" - from ucp_sdk.models.schemas.common.types.card_payment_instrument import ( - Constraints, - ) - - with self.assertRaisesRegex(ValidationError, "[Uu]nique"): - Constraints(brands=["visa", "visa"]) - - @unittest.skip( - "card_payment_instrument.Constraints.brands (uniqueItems) was " - "removed from the schema before the pinned 2026-08-25 UCP release; " - "no current committed model at this path carries a brands field" - ) - def test_brands_accepts_unique_and_none(self) -> None: - """Unique lists and missing values are accepted.""" - from ucp_sdk.models.schemas.common.types.card_payment_instrument import ( - Constraints, - ) - - self.assertEqual( - Constraints(brands=["visa", "mc"]).brands, ["visa", "mc"] - ) - self.assertIsNone(Constraints().brands) - - -class AdditionalPropertiesForbidFinderTest(unittest.TestCase): - """additionalProperties:false objects map to generated class names.""" - - def test_root_titled_object(self) -> None: - with tempfile.TemporaryDirectory() as tmp: - Path(tmp, "error_response.json").write_text( - json.dumps( - { - "title": "Error Response", - "type": "object", - "additionalProperties": False, - "properties": {"messages": {"type": "array"}}, - } - ), - encoding="utf-8", - ) - names = postprocess_models.find_extra_forbid_class_names(Path(tmp)) - self.assertEqual(names, {"ErrorResponse"}) - - def test_nested_untitled_object_uses_property_path(self) -> None: - with tempfile.TemporaryDirectory() as tmp: - Path(tmp, "merchant_fulfillment_config.json").write_text( - json.dumps( - { - "title": "Merchant Fulfillment Config", - "type": "object", - "properties": { - "allows_multi_destination": { - "type": "object", - "additionalProperties": False, - "properties": {"shipping": {"type": "boolean"}}, - } - }, - } - ), - encoding="utf-8", - ) - names = postprocess_models.find_extra_forbid_class_names(Path(tmp)) - self.assertEqual(names, {"AllowsMultiDestination"}) - - def test_loose_and_map_objects_are_excluded(self) -> None: - with tempfile.TemporaryDirectory() as tmp: - Path(tmp, "open.json").write_text( - json.dumps( - { - "title": "Open Object", - "type": "object", - "properties": {"a": {"type": "string"}}, - } - ), - encoding="utf-8", - ) - Path(tmp, "map.json").write_text( - json.dumps( - { - "title": "Map Object", - "type": "object", - "additionalProperties": {"type": "string"}, - "properties": {"a": {"type": "string"}}, - } - ), - encoding="utf-8", - ) - names = postprocess_models.find_extra_forbid_class_names(Path(tmp)) - self.assertEqual(names, set()) - - -class AdditionalPropertiesForbidInjectorTest(unittest.TestCase): - """The injector flips only the target class's model_config to forbid.""" - - SOURCE = '''\ -class AllowsMultiDestination(BaseModel): - """ - Permits multiple destinations per method type. - """ - - model_config = ConfigDict( - extra="allow", - ) - shipping: bool | None = None - - -class MerchantFulfillmentConfig(BaseModel): - """ - Merchant's fulfillment configuration. - """ - - model_config = ConfigDict( - extra="allow", - ) - allows_multi_destination: AllowsMultiDestination | None = None -''' - - def test_flips_only_target_class(self) -> None: - updated = postprocess_models.inject_extra_forbid( - self.SOURCE, "AllowsMultiDestination" - ) - # Target class body now forbids extra keys. - self.assertIn('extra="forbid"', updated) - # The sibling class in the same module keeps extra="allow". - sibling = """class MerchantFulfillmentConfig(BaseModel): - \"\"\" - Merchant's fulfillment configuration. - \"\"\" - - model_config = ConfigDict( - extra="allow", - )""" - self.assertIn(sibling, updated) - - def test_idempotent_after_flip(self) -> None: - once = postprocess_models.inject_extra_forbid( - self.SOURCE, "AllowsMultiDestination" - ) - twice = postprocess_models.inject_extra_forbid( - once, "AllowsMultiDestination" - ) - self.assertEqual(once, twice) - - def test_unknown_class_untouched(self) -> None: - self.assertEqual( - postprocess_models.inject_extra_forbid(self.SOURCE, "Nope"), - self.SOURCE, - ) - - -@unittest.skipUnless( - HAVE_SDK, "requires the installed package (pip install -e .)" -) -class AdditionalPropertiesForbidSemanticTest(unittest.TestCase): - """Committed models reject unknown keys on additionalProperties:false.""" - - def test_error_response_rejects_unknown_keys(self) -> None: - from ucp_sdk.models.schemas.common.types.error_response import ( - ErrorResponse, - ) - - with self.assertRaises(ValidationError): - ErrorResponse.model_validate( - { - "ucp": {"version": "2026-04-08", "status": "error"}, - "messages": [ - { - "type": "error", - "code": "not_found", - "severity": "unrecoverable", - "content": "boom", - } - ], - "bogus": "x", - } - ) - - def test_error_response_accepts_declared_fields(self) -> None: - from ucp_sdk.models.schemas.common.types.error_response import ( - ErrorResponse, - ) - - obj = ErrorResponse.model_validate( - { - "ucp": {"version": "2026-04-08", "status": "error"}, - "messages": [ - { - "type": "error", - "code": "not_found", - "severity": "unrecoverable", - "content": "boom", - } - ], - } - ) - self.assertEqual(obj.messages[0].content, "boom") - - # NOTE(root-cause-0): merchant_fulfillment_config.json was renamed and - # restructured to business_fulfillment_config.json before the pinned - # 2026-08-25 UCP release. The nested additionalProperties:false object - # these tests targeted (allows_multi_destination -> AllowsMultiDestination) - # is gone; the current schema's multi_destination field is a list of - # MultiDestinationItem (extra="allow", no nested forbid object) -- - # verified against - # src/ucp_sdk/models/schemas/shopping/types/business_fulfillment_config.py. - # The HAVE_SDK import gate bug (see the top of this file) had been - # hiding that these two tests could not pass at all, not just that they - # were unrelated to SDK availability. Documented skip rather than silent - # deletion: the additionalProperties:false -> extra="forbid" mechanism - # itself stays covered by test_error_response_rejects_unknown_keys above - # and by AdditionalPropertiesForbidInjectorTest/FinderTest. - @unittest.skip( - "merchant_fulfillment_config.AllowsMultiDestination was removed " - "when the schema was restructured to " - "business_fulfillment_config.MultiDestinationItem before the " - "pinned 2026-08-25 UCP release; no current committed model at " - "this path carries a nested additionalProperties:false object" - ) - def test_allows_multi_destination_rejects_unknown_keys(self) -> None: - from ucp_sdk.models.schemas.shopping.types.business_fulfillment_config import ( - AllowsMultiDestination, - ) - - with self.assertRaises(ValidationError): - AllowsMultiDestination.model_validate( - {"shipping": True, "bogus": "x"} - ) - - @unittest.skip( - "merchant_fulfillment_config.MerchantFulfillmentConfig was renamed " - "and restructured to business_fulfillment_config." - "BusinessFulfillmentConfig before the pinned 2026-08-25 UCP " - "release; see test_allows_multi_destination_rejects_unknown_keys " - "above" - ) - def test_sibling_config_keeps_extra_allow(self) -> None: - from ucp_sdk.models.schemas.shopping.types.business_fulfillment_config import ( - BusinessFulfillmentConfig, - ) - - config = BusinessFulfillmentConfig.model_validate({"bogus": "x"}) - self.assertEqual(config.model_extra, {"bogus": "x"}) - - -@unittest.skipUnless( - HAVE_SDK, "requires the installed package (pip install -e .)" -) -class EntityVersionValidationSemanticTest(unittest.TestCase): - """Committed entity-derived models enforce version pattern validation.""" - - def test_capability_base_accepts_valid_version(self) -> None: - from ucp_sdk.models.schemas.capability import Base - - model = Base.model_validate({"version": "2026-04-08", "id": "test"}) - self.assertEqual(model.version, "2026-04-08") - - def test_capability_base_rejects_invalid_version(self) -> None: - from ucp_sdk.models.schemas.capability import Base - - with self.assertRaises(ValidationError): - Base.model_validate({"version": "not-a-version", "id": "test"}) - - def test_service_base_rejects_invalid_version(self) -> None: - from ucp_sdk.models.schemas.service import Base - - with self.assertRaises(ValidationError): - Base.model_validate({"version": "invalid-format"}) - - def test_payment_handler_base_rejects_invalid_version(self) -> None: - from ucp_sdk.models.schemas.payment_handler import Base - - with self.assertRaises(ValidationError): - Base.model_validate({"version": {"not": "a version"}}) - - -@unittest.skipUnless( - HAVE_SDK, "requires the installed package (pip install -e .)" -) -class JwkConditionalRulesSemanticTest(unittest.TestCase): - """profile.json's jwk_public_key carries five if/then rules, all five - dropped by the generator today: two conditional-required rules (an EC - key needs crv/x/y, an OKP key needs crv/x) and three conditional - const-pin rules pairing a curve with its algorithm (P-256/ES256, - P-384/ES384, Ed25519/EdDSA). Security-adjacent: a profile publishing an - EC key with no curve, or an algorithm that does not match its curve, - currently passes SDK validation and would only fail (or silently - misverify) downstream at signature-verification time. - """ - - def _jwk(self): - from ucp_sdk.models.schemas.profile import JwkPublicKey - - return JwkPublicKey - - def test_ec_key_without_curve_and_coordinates_rejected(self): - with self.assertRaises(ValidationError): - self._jwk()(kid="k1", kty="EC") - - def test_ec_key_with_curve_and_coordinates_accepted(self): - key = self._jwk()( - kid="k1", kty="EC", crv="P-256", x="AA", y="BB", alg="ES256" - ) - self.assertEqual(key.crv, "P-256") - - def test_okp_key_without_curve_rejected(self): - with self.assertRaises(ValidationError): - self._jwk()(kid="k2", kty="OKP") - - def test_okp_key_with_curve_accepted(self): - key = self._jwk()(kid="k2", kty="OKP", crv="Ed25519", x="AA") - self.assertEqual(key.crv, "Ed25519") - - def test_p256_with_mismatched_algorithm_rejected(self): - with self.assertRaises(ValidationError): - self._jwk()( - kid="k3", kty="EC", crv="P-256", x="AA", y="BB", alg="EdDSA" - ) - - def test_p256_with_matching_algorithm_accepted(self): - self._jwk()( - kid="k3", kty="EC", crv="P-256", x="AA", y="BB", alg="ES256" - ) - - def test_p384_with_mismatched_algorithm_rejected(self): - with self.assertRaises(ValidationError): - self._jwk()( - kid="k4", kty="EC", crv="P-384", x="AA", y="BB", alg="ES256" - ) - - def test_p384_with_matching_algorithm_accepted(self): - self._jwk()( - kid="k4", kty="EC", crv="P-384", x="AA", y="BB", alg="ES384" - ) - - def test_ed25519_with_mismatched_algorithm_rejected(self): - with self.assertRaises(ValidationError): - self._jwk()(kid="k5", kty="OKP", crv="Ed25519", x="AA", alg="ES256") - - def test_ed25519_with_matching_algorithm_accepted(self): - self._jwk()(kid="k5", kty="OKP", crv="Ed25519", x="AA", alg="EdDSA") - - def test_algorithm_omitted_is_unconstrained(self): - # alg is optional; verifiers derive it from crv when absent. - self._jwk()(kid="k6", kty="EC", crv="P-256", x="AA", y="BB") - - def test_unrecognized_curve_is_unconstrained(self): - # The crv/kty/alg vocabularies are open (see the schema - # description); a curve outside the three well-known pairings - # carries no algorithm rule. - self._jwk()( - kid="k7", kty="EC", crv="secp256k1", x="AA", y="BB", alg="ES256K" - ) - - -@unittest.skipUnless( - HAVE_SDK, "requires the installed package (pip install -e .)" -) -class UnitScaleSemanticTest(unittest.TestCase): - """unit.json: when unit is C62, scale (if present) MUST be 0.""" - - def _unit(self): - from ucp_sdk.models.schemas.common.types.unit import Unit - - return Unit - - def test_c62_with_nonzero_scale_rejected(self): - with self.assertRaises(ValidationError): - self._unit()(unit="C62", scale=5, display_text="pieces") - - def test_c62_with_zero_scale_accepted(self): - unit = self._unit()(unit="C62", scale=0, display_text="pieces") - self.assertEqual(unit.scale, 0) - - def test_c62_with_scale_omitted_defaults_to_zero(self): - # scale defaults to 0, which already satisfies the C62 pin. - unit = self._unit()(unit="C62", display_text="pieces") - self.assertEqual(unit.scale, 0) - - def test_other_unit_with_nonzero_scale_accepted(self): - # The pin is C62-specific; any other unit is unconstrained. - unit = self._unit()(unit="GRM", scale=3, display_text="grams") - self.assertEqual(unit.scale, 3) - - -@unittest.skipUnless( - HAVE_SDK, "requires the installed package (pip install -e .)" -) -class TimeIntervalDependentRequiredSemanticTest(unittest.TestCase): - """TimeInterval requires opens and closes to be provided together.""" - - def _interval(self): - from ucp_sdk.models.schemas.common.types.time_interval import ( - TimeInterval, - ) - - return TimeInterval - - def _exception_hour(self): - from ucp_sdk.models.schemas.common.types.exception_hour import ( - ExceptionHour, - ) - - return ExceptionHour - - def test_single_opening_time_rejected(self): - with self.assertRaisesRegex(ValidationError, "dependentRequired"): - self._interval()(opens="09:00") - - def test_single_closing_time_rejected(self): - with self.assertRaisesRegex(ValidationError, "dependentRequired"): - self._interval()(closes="17:00") - - def test_empty_and_complete_intervals_accepted(self): - self._interval()() - interval = self._interval()(opens="09:00", closes="17:00") - self.assertEqual((interval.opens, interval.closes), ("09:00", "17:00")) - - def test_explicit_null_still_counts_as_present(self): - interval = self._interval()(opens=None, closes=None) - self.assertEqual(interval.model_fields_set, {"opens", "closes"}) - with self.assertRaisesRegex(ValidationError, "dependentRequired"): - self._interval()(opens=None) - - def test_inherited_interval_rule_is_enforced(self): - with self.assertRaisesRegex(ValidationError, "dependentRequired"): - self._exception_hour()( - valid_from="2026-01-01", - valid_through="2026-01-02", - opens="09:00", - ) - self._exception_hour()( - valid_from="2026-01-01", - valid_through="2026-01-02", - opens="09:00", - closes="17:00", - ) - - -@unittest.skipUnless( - HAVE_SDK, "requires the installed package (pip install -e .)" -) -class FulfillmentMethodDestinationRetypingSemanticTest(unittest.TestCase): - """fulfillment_method.json retypes `destinations` per `type`: a - `shipping` method's destinations are shipping_destination.json items - (`type` const `shipping_address`), a `pickup` method's are - location_destination.json items (`type` const `business_location`). - The committed FulfillmentMethod model, before this fix, accepted any - FulfillmentDestination (bare `type: str`, `id: str`) regardless of the - method's own type, so a `shipping` method could list a - `business_location` destination and it would validate. - """ - - def _method(self): - from ucp_sdk.models.schemas.shopping.types.fulfillment_method import ( - FulfillmentMethod, - ) - - return FulfillmentMethod - - def _destination(self): - from ucp_sdk.models.schemas.shopping.types.fulfillment_destination import ( - FulfillmentDestination, - ) - - return FulfillmentDestination - - def test_shipping_method_with_business_location_destination_rejected( - self, - ): - with self.assertRaises(ValidationError): - self._method()( - id="m1", - type="shipping", - line_item_ids=["li1"], - destinations=[ - self._destination()(type="business_location", id="d1") - ], - ) - - def test_pickup_method_with_shipping_address_destination_rejected(self): - with self.assertRaises(ValidationError): - self._method()( - id="m2", - type="pickup", - line_item_ids=["li1"], - destinations=[ - self._destination()(type="shipping_address", id="d1") - ], - ) - - def test_shipping_method_with_shipping_address_destination_accepted( - self, - ): - method = self._method()( - id="m1", - type="shipping", - line_item_ids=["li1"], - destinations=[ - self._destination()(type="shipping_address", id="d1") - ], - ) - self.assertEqual(method.destinations[0].type, "shipping_address") - - def test_pickup_method_with_business_location_destination_accepted(self): - method = self._method()( - id="m2", - type="pickup", - line_item_ids=["li1"], - destinations=[ - self._destination()(type="business_location", id="d1") - ], - ) - self.assertEqual(method.destinations[0].type, "business_location") - - def test_method_type_outside_the_pinned_vocabulary_is_unconstrained( - self, - ): - # type is an open vocabulary ("Businesses MAY use additional - # values"); only shipping/pickup carry a retyping rule. - self._method()( - id="m3", - type="curbside", - line_item_ids=["li1"], - destinations=[self._destination()(type="anything", id="d1")], - ) - - def test_method_without_destinations_is_unconstrained(self): - self._method()(id="m4", type="shipping", line_item_ids=["li1"]) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..88bd1d3 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,339 @@ +# Copyright 2026 UCP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Rigorous unit tests for OpenAPI 3.1 generated Pydantic models.""" + +import unittest +from typing import cast + +from pydantic import TypeAdapter, ValidationError + +from ucp_sdk.models import ( + Cart, + Checkout, + Order, +) +from ucp_sdk.models.schemas.common.types.amount import Price +from ucp_sdk.models.schemas.common.types.card_payment_instrument import ( + AvailablePaymentInstrument, + CardPaymentInstrument, + PaymentInstrument, +) +from ucp_sdk.models.schemas.common.types.payment import Payment +from ucp_sdk.models.schemas.common.types.totals import ( + Total, +) +from ucp_sdk.models.schemas.shopping.cart import ( + CartCreateRequest, + CartUpdateRequest, +) +from ucp_sdk.models.schemas.shopping.checkout import ( + CheckoutCompleteRequest, + CheckoutCreateRequest, + CheckoutUpdateRequest, +) +from ucp_sdk.models.schemas.shopping.order import ( + OrderCreateRequest, + OrderUpdateRequest, +) +from ucp_sdk.models.schemas.shopping.types.fulfillment_destination import ( + FulfillmentDestination, + LocationDestination, + ShippingDestination, +) +from ucp_sdk.models.schemas.shopping.types.line_item import ( + LineItemCreateRequest, +) + + +class TestModelImportParity(unittest.TestCase): + """Verify modular domain import paths and root exports resolve to identical classes.""" + + def test_root_and_modular_class_identity(self) -> None: + from ucp_sdk.models.schemas.shopping.cart import Cart as ModularCart + from ucp_sdk.models.schemas.shopping.checkout import ( + Checkout as ModularCheckout, + ) + from ucp_sdk.models.schemas.shopping.order import Order as ModularOrder + + self.assertIs(Checkout, ModularCheckout) + self.assertIs(Cart, ModularCart) + self.assertIs(Order, ModularOrder) + + def test_compatibility_aliases(self) -> None: + self.assertIs(CardPaymentInstrument, PaymentInstrument) + + +class TestPolymorphicFulfillmentDestination(unittest.TestCase): + """Verify polymorphic discriminated union deserialization for fulfillment destinations.""" + + def setUp(self) -> None: + self.adapter = TypeAdapter(FulfillmentDestination) + + def test_deserialize_shipping_destination(self) -> None: + raw = { + "id": "dest_ship_001", + "type": "shipping_address", + "street_address": "1600 Amphitheatre Pkwy", + "address_locality": "Mountain View", + "address_region": "CA", + "postal_code": "94043", + "address_country": "US", + } + dest = cast(ShippingDestination, self.adapter.validate_python(raw)) + self.assertIsInstance(dest, ShippingDestination) + self.assertEqual(dest.type, "shipping_address") + self.assertEqual(dest.id, "dest_ship_001") + self.assertEqual(dest.address_locality, "Mountain View") + + def test_deserialize_location_destination(self) -> None: + raw = { + "id": "dest_loc_002", + "type": "business_location", + "name": "Flagship Retail Store", + } + dest = cast(LocationDestination, self.adapter.validate_python(raw)) + self.assertIsInstance(dest, LocationDestination) + self.assertEqual(dest.type, "business_location") + self.assertEqual(dest.id, "dest_loc_002") + self.assertEqual(dest.name, "Flagship Retail Store") + + def test_reject_invalid_destination_type(self) -> None: + raw = { + "id": "dest_err_003", + "type": "drone_teleport", + "name": "Pad 1", + } + with self.assertRaises(ValidationError): + self.adapter.validate_python(raw) + + def test_reject_missing_discriminator(self) -> None: + raw = { + "id": "dest_err_004", + "name": "Unknown", + } + with self.assertRaises(ValidationError): + self.adapter.validate_python(raw) + + +class TestDirectionalRequestSlicing(unittest.TestCase): + """Verify directional request models enforce input-side semantics.""" + + def test_checkout_create_request_valid(self) -> None: + req = CheckoutCreateRequest( + currency="USD", + line_items=[ + LineItemCreateRequest( + item={"id": "item_123"}, + quantity=2, + ) + ], + ) + data = req.model_dump(exclude_none=True) + self.assertEqual(data["currency"], "USD") + self.assertEqual(len(data["line_items"]), 1) + self.assertEqual(data["line_items"][0]["quantity"], 2) + # Verify server-managed fields are not present on CreateRequest + self.assertFalse(hasattr(req, "created_at")) + self.assertFalse(hasattr(req, "completed_at")) + + def test_checkout_update_request(self) -> None: + req = CheckoutUpdateRequest( + currency="EUR", + line_items=[], + ) + self.assertEqual(req.currency, "EUR") + self.assertEqual(req.line_items, []) + + def test_checkout_complete_request(self) -> None: + req = CheckoutCompleteRequest(payment=Payment()) + self.assertIsNotNone(req.payment) + + def test_cart_create_and_update_requests(self) -> None: + req = CartCreateRequest(currency="USD", line_items=[]) + self.assertEqual(req.currency, "USD") + + update = CartUpdateRequest(currency="GBP", line_items=[]) + self.assertEqual(update.currency, "GBP") + + def test_order_create_and_update_requests(self) -> None: + req = OrderCreateRequest( + ucp={"version": "2026-08-25"}, + id="ord_req_1", + checkout_id="chk_1", + permalink_url="https://example.com/orders/ord_req_1", + line_items=[], + fulfillment={"fulfillments": []}, + totals=[], + ) + self.assertEqual(req.id, "ord_req_1") + + update = OrderUpdateRequest( + ucp={"version": "2026-08-25"}, + id="ord_req_1", + checkout_id="chk_1", + permalink_url="https://example.com/orders/ord_req_1", + line_items=[], + fulfillment={"fulfillments": []}, + totals=[], + ) + self.assertEqual(update.id, "ord_req_1") + + +class TestDomainEntitySerialization(unittest.TestCase): + """Verify domain root models serialize and deserialize cleanly.""" + + def test_checkout_roundtrip(self) -> None: + checkout_dict = { + "ucp": {"version": "2026-08-25", "payment_handlers": {}}, + "id": "chk_test_999", + "status": "ready_for_complete", + "currency": "USD", + "line_items": [ + { + "id": "li_1", + "item": {"id": "prod_1", "title": "Widget", "price": 2500}, + "quantity": 1, + "totals": [ + { + "type": "subtotal", + "amount": 2500, + } + ], + } + ], + "totals": [ + { + "type": "total", + "amount": 2500, + }, + ], + "links": [], + } + checkout = Checkout.model_validate(checkout_dict) + self.assertEqual(checkout.id, "chk_test_999") + self.assertEqual(checkout.currency, "USD") + self.assertEqual(len(checkout.line_items), 1) + self.assertEqual(checkout.line_items[0].id, "li_1") + + dumped = checkout.model_dump(exclude_none=True) + self.assertEqual(dumped["id"], "chk_test_999") + self.assertEqual(dumped["currency"], "USD") + self.assertEqual(dumped["status"], "ready_for_complete") + + def test_cart_roundtrip(self) -> None: + cart_dict = { + "ucp": {"version": "2026-08-25"}, + "id": "cart_test_123", + "currency": "USD", + "line_items": [], + "totals": [], + } + cart = Cart.model_validate(cart_dict) + self.assertEqual(cart.id, "cart_test_123") + dumped = cart.model_dump(exclude_none=True) + self.assertEqual(dumped["id"], "cart_test_123") + + def test_order_roundtrip(self) -> None: + order_dict = { + "ucp": {"version": "2026-08-25"}, + "id": "ord_test_456", + "checkout_id": "chk_test_999", + "permalink_url": "https://example.com/orders/ord_test_456", + "line_items": [], + "fulfillment": {"fulfillments": []}, + "currency": "USD", + "totals": [], + } + order = Order.model_validate(order_dict) + self.assertEqual(order.id, "ord_test_456") + dumped = order.model_dump(exclude_none=True) + self.assertEqual(dumped["id"], "ord_test_456") + + +class TestCommonTypes(unittest.TestCase): + """Verify common auxiliary types.""" + + def test_totals_and_amounts(self) -> None: + total = Total(type="tax", amount=150) + self.assertEqual(total.type, "tax") + self.assertEqual(total.amount, 150) + + price = Price(amount=5000, currency="USD") + self.assertEqual(price.amount, 5000) + self.assertEqual(price.currency, "USD") + + def test_payment_instrument(self) -> None: + inst = AvailablePaymentInstrument( + type="card", + ) + self.assertEqual(inst.type, "card") + + +if __name__ == "__main__": + unittest.main() + + +class TestValidationInvariants(unittest.TestCase): + """Verify strict Pydantic v2 type checking and validation invariants.""" + + def test_amount_type_validation(self) -> None: + # Price amount must be integer (cents/minor units) + with self.assertRaises(ValidationError): + Price(amount="not_a_number", currency="USD") # type: ignore[arg-type] + + def test_signals_validation(self) -> None: + from ucp_sdk.models.schemas.common.types.signals import Signals + + sig = Signals(ip="192.168.1.1", user_agent="Mozilla/5.0") + self.assertEqual(sig.ip, "192.168.1.1") + self.assertEqual(sig.user_agent, "Mozilla/5.0") + + def test_profile_and_jwk(self) -> None: + from ucp_sdk.models.schemas.profile import JwkPublicKey, Profile + + key = JwkPublicKey( + kid="key-1", kty="EC", crv="P-256", x="base64_x", y="base64_y" + ) + self.assertEqual(key.kty, "EC") + self.assertEqual(key.kid, "key-1") + + prof = Profile( + ucp={"version": "2026-08-25"}, + id="prof_001", + name="Test Merchant", + url="https://merchant.example.com", + keys=[key], + ) + self.assertEqual(prof.id, "prof_001") + self.assertEqual(len(prof.keys), 1) + + def test_error_response_models(self) -> None: + from ucp_sdk.models.schemas.common.types.error_response import ( + ErrorResponse, + ) + from ucp_sdk.models.schemas.common.types.message import MessageError + + msg = MessageError( + content="Malformed payload", + severity="unrecoverable", + code="invalid_request", + ) + resp = ErrorResponse( + ucp={"version": "2026-08-25"}, + messages=[msg], + ) + self.assertEqual(len(resp.messages), 1) + self.assertEqual(resp.messages[0].code, "invalid_request") + self.assertEqual(resp.messages[0].content, "Malformed payload")