diff --git a/.gitignore b/.gitignore index 2874c4a..dfb3584 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules/ __pycache__/ .venv/ *.py[cod] +ucp/ diff --git a/generate_models.sh b/generate_models.sh index 95cd086..eeab517 100755 --- a/generate_models.sh +++ b/generate_models.sh @@ -56,7 +56,7 @@ rm -rf "$RAW_SCHEMA_DIR" cp -R "$SCHEMA_DIR" "$RAW_SCHEMA_DIR" echo "Preprocessing schemas..." -uv run python preprocess_schemas.py +uv run --no-sync python preprocess_schemas.py echo "Generating Pydantic models from preprocessed schemas..." @@ -77,9 +77,8 @@ mkdir -p "$OUTPUT_DIR" # 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. -uv run \ - --link-mode=copy \ - --extra-index-url https://pypi.org/simple python \ +uv run --no-sync \ + --link-mode=copy python \ -m datamodel_code_generator \ --input "$SCHEMA_DIR" \ --input-file-type jsonschema \ @@ -99,11 +98,11 @@ uv run \ echo "Post-processing generated models (constraints the generator ignores)..." -uv run python postprocess_models.py || exit 1 +uv run --no-sync python postprocess_models.py || exit 1 echo "Formatting generated models..." -uv run ruff format -uv run ruff check --fix "$OUTPUT_DIR" +uv run --no-sync ruff format +uv run --no-sync ruff check --fix "$OUTPUT_DIR" echo "Done. Models generated in $OUTPUT_DIR" diff --git a/postprocess_models.py b/postprocess_models.py index 3da3c9b..0ad3a0b 100644 --- a/postprocess_models.py +++ b/postprocess_models.py @@ -667,7 +667,10 @@ def inject_array_contains(source, alias_name, groups, item_condition=None): break if close is None: return source - out = source[:close] + f", AfterValidator({func_name})" + source[close:] + before = source[:close].rstrip() + if before.endswith(","): + before = before[:-1].rstrip() + out = before + f", AfterValidator({func_name})" + source[close:] 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:] @@ -1434,6 +1437,48 @@ def _patch_extra_forbid(): return patched, 0 +def _patch_cart_checkout_create_request(): + """Ensure Checkout in cart_create_request allows cart_id or line_items.""" + path = OUTPUT_DIR / "shopping" / "cart_create_request.py" + if not path.exists(): + return 0, 0 + source = path.read_text(encoding="utf-8") + if "_enforce_cart_conversion" in source: + return 0, 0 + + class_match = re.search( + r"^class Checkout\(CheckoutCreateRequest\):", source, re.M + ) + if not class_match: + return 0, 0 + + validator_code = ''' line_items: list[line_item_create_request.LineItemCreateRequest] | None = None + + @model_validator(mode="after") + def _enforce_cart_conversion(self): + """Require either cart_id or line_items for checkout creation.""" + if not getattr(self, "cart_id", None) and not getattr(self, "line_items", None): + raise ValueError("Either cart_id or line_items must be provided") + return self +''' + config_match = re.search( + r"model_config = ConfigDict\(\s*extra=\"allow\",?\s*\)", + source[class_match.start() :], + ) + if config_match: + insert_pos = class_match.start() + config_match.end() + source = ( + source[:insert_pos] + "\n" + validator_code + source[insert_pos:] + ) + source = _ensure_pydantic_import(source, "model_validator") + path.write_text(source, encoding="utf-8") + sys.stdout.write( + f" cart conversion validator on 'Checkout' -> {path}\n" + ) + return 1, 0 + return 0, 0 + + def main(): """Main entry point to scan schemas and patch generated models.""" patched_mp, rc_mp = _patch_min_properties() @@ -1443,6 +1488,7 @@ def main(): patched_cb, rc_cb = _patch_conditional_bounds() patched_ui, rc_ui = _patch_unique_items() patched_ef, rc_ef = _patch_extra_forbid() + patched_cc, rc_cc = _patch_cart_checkout_create_request() total = ( patched_mp + patched_pn @@ -1451,9 +1497,10 @@ def main(): + patched_cb + patched_ui + patched_ef + + patched_cc ) sys.stdout.write(f"postprocess: {total} module(s) patched\n") - return rc_mp or rc_pn or rc_ac or rc_cr or rc_cb or rc_ui or rc_ef + return rc_mp or rc_pn or rc_ac or rc_cr or rc_cb or rc_ui or rc_ef or rc_cc if __name__ == "__main__": diff --git a/preprocess_schemas.py b/preprocess_schemas.py index cd0041a..3b80383 100644 --- a/preprocess_schemas.py +++ b/preprocess_schemas.py @@ -377,21 +377,26 @@ class name like 'Checkout'); fall back to dot-replaced-with-underscore def get_required_ops(schema): """ - Scans a schema for the custom 'ucp_request' metadata. + Scans a schema's properties and $defs for 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()) + containers = [] + if isinstance(schema.get("properties"), dict): + containers.append(schema["properties"]) + if isinstance(schema.get("$defs"), dict): + containers.append(schema["$defs"]) + + for container in containers: + for node in iter_nodes(container): + if not isinstance(node, dict): + continue + marker = node.get("ucp_request") + if marker is not None: + if isinstance(marker, str): + ops.update(["create", "update"]) # Standard shortcut + elif isinstance(marker, dict): + ops.update(marker.keys()) return ops @@ -527,6 +532,22 @@ def _create_single_variant( variant, op, file_path, global_variant_requirements ) + # Apply request rules to top-level definitions in $defs + defs = variant.get("$defs", {}) + if isinstance(defs, dict): + for node in defs.values(): + if isinstance(node, dict) and ( + "properties" in node or node.get("type") == "object" + ): + _apply_request_rules_to_object( + node, op, file_path, global_variant_requirements + ) + + # Rewrite all external references across the entire variant tree + rewrite_refs_to_variants( + variant, op, file_path, global_variant_requirements + ) + return variant @@ -595,20 +616,23 @@ def normalize_metadata_schemas(schemas, target_dir): def extract_external_refs(schema, path): - """Finds all relative external file references in the schema properties.""" + """Finds all relative external file references in properties and $defs.""" refs = [] - props = schema.get("properties", {}) - if not isinstance(props, dict): - return refs - - for name, data in props.items(): - 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)) + containers = [] + if isinstance(schema.get("properties"), dict): + containers.append(schema["properties"]) + if isinstance(schema.get("$defs"), dict): + containers.append(schema["$defs"]) + + for container in containers: + for name, data in container.items(): + 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)) return refs @@ -629,10 +653,10 @@ def propagate_needs_transitive(variant_needs, schema_refs, schemas): if child_path not in schemas: continue - # Only propagate if the property isn't 'omit'ted for this op - data = ( - schemas[path].get("properties", {}).get(prop_name, {}) - ) + # Check properties first, then $defs for any operation override + data = schemas[path].get("properties", {}).get( + prop_name + ) or schemas[path].get("$defs", {}).get(prop_name, {}) include, _ = eval_prop_inclusion( prop_name, data, op, schemas[path].get("required", []) ) diff --git a/src/ucp_sdk/models/schemas/__init__.py b/src/ucp_sdk/models/schemas/__init__.py index 1252d6b..421dc21 100644 --- a/src/ucp_sdk/models/schemas/__init__.py +++ b/src/ucp_sdk/models/schemas/__init__.py @@ -15,3 +15,4 @@ # generated by datamodel-codegen # pylint: disable=all # pyformat: disable + diff --git a/src/ucp_sdk/models/schemas/capability_complete_request.py b/src/ucp_sdk/models/schemas/capability_complete_request.py new file mode 100644 index 0000000..567916a --- /dev/null +++ b/src/ucp_sdk/models/schemas/capability_complete_request.py @@ -0,0 +1,236 @@ +# 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 + +UcpCapabilityCompleteRequest = TypeAliasType( + "UcpCapabilityCompleteRequest", + Annotated[Any, Field(..., title="UCP Capability Complete Request")], +) +""" +Schema for UCP capabilities and extensions. Extensions are capabilities with an 'extends' field. Uses reverse-domain naming for governance. +""" + + +Extends = TypeAliasType( + "Extends", + Annotated[ + str, Field(..., pattern="^[a-z][a-z0-9]*(?:\\.[a-z][a-z0-9_]*)+$") + ], +) +""" +Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. +""" + + +Extends9Item = TypeAliasType( + "Extends9Item", + Annotated[ + str, Field(..., pattern="^[a-z][a-z0-9]*(?:\\.[a-z][a-z0-9_]*)+$") + ], +) + + +Extends9 = TypeAliasType( + "Extends9", Annotated[list[Extends9Item], Field(..., min_length=1)] +) +""" +Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. +""" + + +Extends10 = TypeAliasType("Extends10", Extends) + + +Extends11Item = TypeAliasType("Extends11Item", Extends9Item) + + +Extends11 = TypeAliasType( + "Extends11", Annotated[list[Extends11Item], Field(..., min_length=1)] +) +""" +Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. +""" + + +Extends12 = TypeAliasType("Extends12", Extends) + + +Extends13Item = TypeAliasType("Extends13Item", Extends9Item) + + +Extends13 = TypeAliasType( + "Extends13", Annotated[list[Extends13Item], Field(..., min_length=1)] +) +""" +Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. +""" + + +Extends14 = TypeAliasType("Extends14", Extends) + + +Extends15Item = TypeAliasType("Extends15Item", Extends9Item) + + +Extends15 = TypeAliasType( + "Extends15", Annotated[list[Extends15Item], Field(..., min_length=1)] +) +""" +Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. +""" + + +Version = TypeAliasType("Version", Any) + + +class Base(BaseModel): + 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. + """ + extends: Extends | Extends9 | 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: Version + """ + 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: Extends10 | Extends11 | None = None + """ + Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. + """ + + +class BusinessSchema(BaseModel): + """ + Capability configuration for business/merchant level. May include business-specific config overrides. + """ + + 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. + """ + extends: Extends12 | Extends13 | 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: 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. + """ + extends: Extends14 | Extends15 | None = None + """ + Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. + """ diff --git a/src/ucp_sdk/models/schemas/capability_create_request.py b/src/ucp_sdk/models/schemas/capability_create_request.py new file mode 100644 index 0000000..8f8f6a0 --- /dev/null +++ b/src/ucp_sdk/models/schemas/capability_create_request.py @@ -0,0 +1,236 @@ +# 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 + +UcpCapabilityCreateRequest = TypeAliasType( + "UcpCapabilityCreateRequest", + Annotated[Any, Field(..., title="UCP Capability Create Request")], +) +""" +Schema for UCP capabilities and extensions. Extensions are capabilities with an 'extends' field. Uses reverse-domain naming for governance. +""" + + +Extends = TypeAliasType( + "Extends", + Annotated[ + str, Field(..., pattern="^[a-z][a-z0-9]*(?:\\.[a-z][a-z0-9_]*)+$") + ], +) +""" +Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. +""" + + +Extends17Item = TypeAliasType( + "Extends17Item", + Annotated[ + str, Field(..., pattern="^[a-z][a-z0-9]*(?:\\.[a-z][a-z0-9_]*)+$") + ], +) + + +Extends17 = TypeAliasType( + "Extends17", Annotated[list[Extends17Item], Field(..., min_length=1)] +) +""" +Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. +""" + + +Extends18 = TypeAliasType("Extends18", Extends) + + +Extends19Item = TypeAliasType("Extends19Item", Extends17Item) + + +Extends19 = TypeAliasType( + "Extends19", Annotated[list[Extends19Item], Field(..., min_length=1)] +) +""" +Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. +""" + + +Extends20 = TypeAliasType("Extends20", Extends) + + +Extends21Item = TypeAliasType("Extends21Item", Extends17Item) + + +Extends21 = TypeAliasType( + "Extends21", Annotated[list[Extends21Item], Field(..., min_length=1)] +) +""" +Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. +""" + + +Extends22 = TypeAliasType("Extends22", Extends) + + +Extends23Item = TypeAliasType("Extends23Item", Extends17Item) + + +Extends23 = TypeAliasType( + "Extends23", Annotated[list[Extends23Item], Field(..., min_length=1)] +) +""" +Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. +""" + + +Version = TypeAliasType("Version", Any) + + +class Base(BaseModel): + 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. + """ + extends: Extends | Extends17 | 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: Version + """ + 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: Extends18 | Extends19 | None = None + """ + Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. + """ + + +class BusinessSchema(BaseModel): + """ + Capability configuration for business/merchant level. May include business-specific config overrides. + """ + + 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. + """ + extends: Extends20 | Extends21 | 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: 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. + """ + extends: Extends22 | Extends23 | None = None + """ + Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. + """ diff --git a/src/ucp_sdk/models/schemas/capability_update_request.py b/src/ucp_sdk/models/schemas/capability_update_request.py new file mode 100644 index 0000000..7adbced --- /dev/null +++ b/src/ucp_sdk/models/schemas/capability_update_request.py @@ -0,0 +1,236 @@ +# 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 + +UcpCapabilityUpdateRequest = TypeAliasType( + "UcpCapabilityUpdateRequest", + Annotated[Any, Field(..., title="UCP Capability Update Request")], +) +""" +Schema for UCP capabilities and extensions. Extensions are capabilities with an 'extends' field. Uses reverse-domain naming for governance. +""" + + +Extends = TypeAliasType( + "Extends", + Annotated[ + str, Field(..., pattern="^[a-z][a-z0-9]*(?:\\.[a-z][a-z0-9_]*)+$") + ], +) +""" +Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. +""" + + +Extends25Item = TypeAliasType( + "Extends25Item", + Annotated[ + str, Field(..., pattern="^[a-z][a-z0-9]*(?:\\.[a-z][a-z0-9_]*)+$") + ], +) + + +Extends25 = TypeAliasType( + "Extends25", Annotated[list[Extends25Item], Field(..., min_length=1)] +) +""" +Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. +""" + + +Extends26 = TypeAliasType("Extends26", Extends) + + +Extends27Item = TypeAliasType("Extends27Item", Extends25Item) + + +Extends27 = TypeAliasType( + "Extends27", Annotated[list[Extends27Item], Field(..., min_length=1)] +) +""" +Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. +""" + + +Extends28 = TypeAliasType("Extends28", Extends) + + +Extends29Item = TypeAliasType("Extends29Item", Extends25Item) + + +Extends29 = TypeAliasType( + "Extends29", Annotated[list[Extends29Item], Field(..., min_length=1)] +) +""" +Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. +""" + + +Extends30 = TypeAliasType("Extends30", Extends) + + +Extends31Item = TypeAliasType("Extends31Item", Extends25Item) + + +Extends31 = TypeAliasType( + "Extends31", Annotated[list[Extends31Item], Field(..., min_length=1)] +) +""" +Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. +""" + + +Version = TypeAliasType("Version", Any) + + +class Base(BaseModel): + 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. + """ + extends: Extends | Extends25 | 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: Version + """ + 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: Extends26 | Extends27 | None = None + """ + Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. + """ + + +class BusinessSchema(BaseModel): + """ + Capability configuration for business/merchant level. May include business-specific config overrides. + """ + + 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. + """ + extends: Extends28 | Extends29 | 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: 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. + """ + extends: Extends30 | Extends31 | None = None + """ + Parent capability(s) this extends. Present for extensions, absent for root capabilities. Use array for multi-parent extensions. + """ diff --git a/src/ucp_sdk/models/schemas/common/__init__.py b/src/ucp_sdk/models/schemas/common/__init__.py index 1252d6b..421dc21 100644 --- a/src/ucp_sdk/models/schemas/common/__init__.py +++ b/src/ucp_sdk/models/schemas/common/__init__.py @@ -15,3 +15,4 @@ # generated by datamodel-codegen # pylint: disable=all # pyformat: disable + diff --git a/src/ucp_sdk/models/schemas/service.py b/src/ucp_sdk/models/schemas/service.py index efe34c7..619e523 100644 --- a/src/ucp_sdk/models/schemas/service.py +++ b/src/ucp_sdk/models/schemas/service.py @@ -124,7 +124,7 @@ class PlatformSchema(BaseModel): """ -class PlatformSchema7(BaseModel): +class PlatformSchema10(BaseModel): """ Full service declaration for platform-level discovery. All transports require `version`, `spec`, and `transport`. REST, MCP, and embedded additionally require `schema`. """ @@ -162,7 +162,7 @@ class PlatformSchema7(BaseModel): """ -class PlatformSchema8(BaseModel): +class PlatformSchema11(BaseModel): """ Full service declaration for platform-level discovery. All transports require `version`, `spec`, and `transport`. REST, MCP, and embedded additionally require `schema`. """ @@ -200,7 +200,7 @@ class PlatformSchema8(BaseModel): """ -class PlatformSchema9(BaseModel): +class PlatformSchema12(BaseModel): """ Full service declaration for platform-level discovery. All transports require `version`, `spec`, and `transport`. REST, MCP, and embedded additionally require `schema`. """ @@ -238,10 +238,10 @@ class PlatformSchema9(BaseModel): """ -PlatformSchema5 = TypeAliasType( - "PlatformSchema5", +PlatformSchema8 = TypeAliasType( + "PlatformSchema8", Annotated[ - PlatformSchema | PlatformSchema7 | PlatformSchema8 | PlatformSchema9, + PlatformSchema | PlatformSchema10 | PlatformSchema11 | PlatformSchema12, Field(..., title="Service (Platform Schema)"), ], ) @@ -288,7 +288,7 @@ class BusinessSchema(BaseModel): """ -class BusinessSchema4(BaseModel): +class BusinessSchema7(BaseModel): """ Service binding for business/merchant configuration. May override platform endpoints. """ @@ -326,7 +326,7 @@ class BusinessSchema4(BaseModel): """ -class BusinessSchema5(BaseModel): +class BusinessSchema8(BaseModel): """ Service binding for business/merchant configuration. May override platform endpoints. """ @@ -364,7 +364,7 @@ class BusinessSchema5(BaseModel): """ -class BusinessSchema6(BaseModel): +class BusinessSchema9(BaseModel): """ Service binding for business/merchant configuration. May override platform endpoints. """ @@ -402,10 +402,10 @@ class BusinessSchema6(BaseModel): """ -BusinessSchema2 = TypeAliasType( - "BusinessSchema2", +BusinessSchema5 = TypeAliasType( + "BusinessSchema5", Annotated[ - BusinessSchema | BusinessSchema4 | BusinessSchema5 | BusinessSchema6, + BusinessSchema | BusinessSchema7 | BusinessSchema8 | BusinessSchema9, Field(..., title="Service (Business Schema)"), ], ) @@ -452,7 +452,7 @@ class ResponseSchema(BaseModel): """ -class ResponseSchema4(BaseModel): +class ResponseSchema7(BaseModel): """ Service binding in API responses. Includes per-resource transport configuration via typed config. """ @@ -490,7 +490,7 @@ class ResponseSchema4(BaseModel): """ -class ResponseSchema5(BaseModel): +class ResponseSchema8(BaseModel): """ Service binding in API responses. Includes per-resource transport configuration via typed config. """ @@ -528,7 +528,7 @@ class ResponseSchema5(BaseModel): """ -class ResponseSchema6(BaseModel): +class ResponseSchema9(BaseModel): """ Service binding in API responses. Includes per-resource transport configuration via typed config. """ @@ -566,10 +566,10 @@ class ResponseSchema6(BaseModel): """ -ResponseSchema2 = TypeAliasType( - "ResponseSchema2", +ResponseSchema5 = TypeAliasType( + "ResponseSchema5", Annotated[ - ResponseSchema | ResponseSchema4 | ResponseSchema5 | ResponseSchema6, + ResponseSchema | ResponseSchema7 | ResponseSchema8 | ResponseSchema9, Field(..., title="Service (Response Schema)"), ], ) diff --git a/src/ucp_sdk/models/schemas/shopping/__init__.py b/src/ucp_sdk/models/schemas/shopping/__init__.py index 1252d6b..421dc21 100644 --- a/src/ucp_sdk/models/schemas/shopping/__init__.py +++ b/src/ucp_sdk/models/schemas/shopping/__init__.py @@ -15,3 +15,4 @@ # generated by datamodel-codegen # pylint: disable=all # pyformat: disable + diff --git a/src/ucp_sdk/models/schemas/shopping/ap2_mandate_complete_request.py b/src/ucp_sdk/models/schemas/shopping/ap2_mandate_complete_request.py new file mode 100644 index 0000000..c473d05 --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/ap2_mandate_complete_request.py @@ -0,0 +1,142 @@ +# 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 .checkout_complete_request import CheckoutCompleteRequest + +Ap2MandateExtensionCompleteRequest = TypeAliasType( + "Ap2MandateExtensionCompleteRequest", + Annotated[Any, Field(..., title="AP2 Mandate Extension Complete Request")], +) +""" +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", + ) + + +class Ap2WithCheckoutMandate(BaseModel): + """ + AP2 extension data including checkout mandate. + """ + + model_config = ConfigDict( + extra="allow", + ) + checkout_mandate: CheckoutMandate + """ + 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(CheckoutCompleteRequest): + """ + Checkout extended with AP2 mandate support. + """ + + model_config = ConfigDict( + extra="allow", + ) + ap2: Ap2 + """ + AP2 extension data including merchant authorization. + """ diff --git a/src/ucp_sdk/models/schemas/shopping/ap2_mandate_create_request.py b/src/ucp_sdk/models/schemas/shopping/ap2_mandate_create_request.py new file mode 100644 index 0000000..8fa7325 --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/ap2_mandate_create_request.py @@ -0,0 +1,116 @@ +# 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 .checkout_create_request import CheckoutCreateRequest + +Ap2MandateExtensionCreateRequest = TypeAliasType( + "Ap2MandateExtensionCreateRequest", + Annotated[Any, Field(..., title="AP2 Mandate Extension Create Request")], +) +""" +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", + ) + + +class Ap2WithCheckoutMandate(BaseModel): + """ + AP2 extension data including checkout mandate. + """ + + model_config = ConfigDict( + extra="allow", + ) + + +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 Checkout(CheckoutCreateRequest): + """ + Checkout extended with AP2 mandate support. + """ + + model_config = ConfigDict( + extra="allow", + ) diff --git a/src/ucp_sdk/models/schemas/shopping/ap2_mandate_update_request.py b/src/ucp_sdk/models/schemas/shopping/ap2_mandate_update_request.py new file mode 100644 index 0000000..89faf47 --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/ap2_mandate_update_request.py @@ -0,0 +1,116 @@ +# 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 .checkout_update_request import CheckoutUpdateRequest + +Ap2MandateExtensionUpdateRequest = TypeAliasType( + "Ap2MandateExtensionUpdateRequest", + Annotated[Any, Field(..., title="AP2 Mandate Extension Update Request")], +) +""" +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", + ) + + +class Ap2WithCheckoutMandate(BaseModel): + """ + AP2 extension data including checkout mandate. + """ + + model_config = ConfigDict( + extra="allow", + ) + + +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 Checkout(CheckoutUpdateRequest): + """ + Checkout extended with AP2 mandate support. + """ + + model_config = ConfigDict( + extra="allow", + ) diff --git a/src/ucp_sdk/models/schemas/shopping/buyer_consent_complete_request.py b/src/ucp_sdk/models/schemas/shopping/buyer_consent_complete_request.py new file mode 100644 index 0000000..23db4ae --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/buyer_consent_complete_request.py @@ -0,0 +1,87 @@ +# 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 .checkout_complete_request import CheckoutCompleteRequest +from .types.buyer_complete_request import BuyerCompleteRequest + +BuyerConsentExtensionCompleteRequest = TypeAliasType( + "BuyerConsentExtensionCompleteRequest", + Annotated[ + Any, Field(..., title="Buyer Consent Extension Complete Request") + ], +) +""" +Extends Checkout with buyer consent tracking for privacy compliance via the buyer object. +""" + + +class Consent(BaseModel): + """ + User consent states for data processing + """ + + model_config = ConfigDict( + extra="allow", + ) + analytics: bool | None = None + """ + Consent for analytics and performance tracking. + """ + preferences: bool | None = None + """ + Consent for storing user preferences. + """ + marketing: bool | None = None + """ + Consent for marketing communications. + """ + sale_of_data: bool | None = None + """ + Consent for selling data to third parties (CCPA). + """ + + +class Buyer(BuyerCompleteRequest): + """ + Buyer object extended with consent tracking. + """ + + model_config = ConfigDict( + extra="allow", + ) + consent: Consent | None = None + """ + Consent tracking fields. + """ + + +class Checkout(CheckoutCompleteRequest): + """ + Checkout extended with consent tracking via buyer object. + """ + + model_config = ConfigDict( + extra="allow", + ) diff --git a/src/ucp_sdk/models/schemas/shopping/buyer_consent_create_request.py b/src/ucp_sdk/models/schemas/shopping/buyer_consent_create_request.py new file mode 100644 index 0000000..ab38046 --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/buyer_consent_create_request.py @@ -0,0 +1,89 @@ +# 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 .checkout_create_request import CheckoutCreateRequest +from .types.buyer_create_request import BuyerCreateRequest + +BuyerConsentExtensionCreateRequest = TypeAliasType( + "BuyerConsentExtensionCreateRequest", + Annotated[Any, Field(..., title="Buyer Consent Extension Create Request")], +) +""" +Extends Checkout with buyer consent tracking for privacy compliance via the buyer object. +""" + + +class Consent(BaseModel): + """ + User consent states for data processing + """ + + model_config = ConfigDict( + extra="allow", + ) + analytics: bool | None = None + """ + Consent for analytics and performance tracking. + """ + preferences: bool | None = None + """ + Consent for storing user preferences. + """ + marketing: bool | None = None + """ + Consent for marketing communications. + """ + sale_of_data: bool | None = None + """ + Consent for selling data to third parties (CCPA). + """ + + +class Buyer(BuyerCreateRequest): + """ + Buyer object extended with consent tracking. + """ + + model_config = ConfigDict( + extra="allow", + ) + consent: Consent | None = None + """ + Consent tracking fields. + """ + + +class Checkout(CheckoutCreateRequest): + """ + Checkout extended with consent tracking via buyer object. + """ + + model_config = ConfigDict( + extra="allow", + ) + buyer: Buyer | None = None + """ + Buyer with consent tracking. + """ diff --git a/src/ucp_sdk/models/schemas/shopping/buyer_consent_update_request.py b/src/ucp_sdk/models/schemas/shopping/buyer_consent_update_request.py new file mode 100644 index 0000000..36c27a8 --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/buyer_consent_update_request.py @@ -0,0 +1,89 @@ +# 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 .checkout_update_request import CheckoutUpdateRequest +from .types.buyer_update_request import BuyerUpdateRequest + +BuyerConsentExtensionUpdateRequest = TypeAliasType( + "BuyerConsentExtensionUpdateRequest", + Annotated[Any, Field(..., title="Buyer Consent Extension Update Request")], +) +""" +Extends Checkout with buyer consent tracking for privacy compliance via the buyer object. +""" + + +class Consent(BaseModel): + """ + User consent states for data processing + """ + + model_config = ConfigDict( + extra="allow", + ) + analytics: bool | None = None + """ + Consent for analytics and performance tracking. + """ + preferences: bool | None = None + """ + Consent for storing user preferences. + """ + marketing: bool | None = None + """ + Consent for marketing communications. + """ + sale_of_data: bool | None = None + """ + Consent for selling data to third parties (CCPA). + """ + + +class Buyer(BuyerUpdateRequest): + """ + Buyer object extended with consent tracking. + """ + + model_config = ConfigDict( + extra="allow", + ) + consent: Consent | None = None + """ + Consent tracking fields. + """ + + +class Checkout(CheckoutUpdateRequest): + """ + Checkout extended with consent tracking via buyer object. + """ + + model_config = ConfigDict( + extra="allow", + ) + buyer: Buyer | None = None + """ + Buyer with consent tracking. + """ diff --git a/src/ucp_sdk/models/schemas/shopping/cart_complete_request.py b/src/ucp_sdk/models/schemas/shopping/cart_complete_request.py new file mode 100644 index 0000000..bcee79d --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/cart_complete_request.py @@ -0,0 +1,43 @@ +# 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 .checkout_complete_request import CheckoutCompleteRequest + + +class CartCompleteRequest(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", + ) + + +class Checkout(CheckoutCompleteRequest): + """ + Checkout extended with cart capability. Adds cart_id to create_checkout for cart-to-checkout conversion. + """ + + model_config = ConfigDict( + extra="allow", + ) diff --git a/src/ucp_sdk/models/schemas/shopping/cart_create_request.py b/src/ucp_sdk/models/schemas/shopping/cart_create_request.py index 636e8aa..ab12782 100644 --- a/src/ucp_sdk/models/schemas/shopping/cart_create_request.py +++ b/src/ucp_sdk/models/schemas/shopping/cart_create_request.py @@ -18,9 +18,9 @@ from __future__ import annotations -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, model_validator -from .checkout import Checkout as Checkout_1 +from .checkout_create_request import CheckoutCreateRequest from .types import ( attribution_create_request, buyer_create_request, @@ -56,7 +56,7 @@ class CartCreateRequest(BaseModel): """ -class Checkout(Checkout_1): +class Checkout(CheckoutCreateRequest): """ Checkout extended with cart capability. Adds cart_id to create_checkout for cart-to-checkout conversion. """ @@ -64,6 +64,19 @@ class Checkout(Checkout_1): model_config = ConfigDict( extra="allow", ) + line_items: list[line_item_create_request.LineItemCreateRequest] | None = ( + None + ) + + @model_validator(mode="after") + def _enforce_cart_conversion(self): + """Require either cart_id or line_items for checkout creation.""" + if not getattr(self, "cart_id", None) and not getattr( + self, "line_items", None + ): + raise ValueError("Either cart_id or line_items must be provided") + return self + 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 index c4d35f9..32831f0 100644 --- a/src/ucp_sdk/models/schemas/shopping/cart_update_request.py +++ b/src/ucp_sdk/models/schemas/shopping/cart_update_request.py @@ -20,7 +20,7 @@ from pydantic import BaseModel, ConfigDict -from .checkout import Checkout as Checkout_1 +from .checkout_update_request import CheckoutUpdateRequest from .types import ( attribution_update_request, buyer_update_request, @@ -60,7 +60,7 @@ class CartUpdateRequest(BaseModel): """ -class Checkout(Checkout_1): +class Checkout(CheckoutUpdateRequest): """ Checkout extended with cart capability. Adds cart_id to create_checkout for cart-to-checkout conversion. """ @@ -68,7 +68,3 @@ class Checkout(Checkout_1): 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/discount_complete_request.py b/src/ucp_sdk/models/schemas/shopping/discount_complete_request.py new file mode 100644 index 0000000..79c55f0 --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/discount_complete_request.py @@ -0,0 +1,137 @@ +# 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 .cart_complete_request import CartCompleteRequest +from .checkout_complete_request import CheckoutCompleteRequest +from .types import amount_complete_request, reverse_domain_name_complete_request + +DiscountExtensionCompleteRequest = TypeAliasType( + "DiscountExtensionCompleteRequest", + Annotated[Any, Field(..., title="Discount Extension Complete Request")], +) +""" +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 + """ + JSONPath to the allocation target (e.g., '$.line_items[0]', '$.totals.shipping'). + """ + amount: amount_complete_request.AmountCompleteRequest + """ + Amount allocated to this target in ISO 4217 minor units. + """ + + +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. + """ + + +class Cart(CartCompleteRequest): + """ + Cart extended with discount capability. + """ + + model_config = ConfigDict( + extra="allow", + ) + + +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_complete_request.AmountCompleteRequest + """ + 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_complete_request.ReverseDomainNameCompleteRequest + | 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 Checkout(CheckoutCompleteRequest): + """ + Checkout extended with discount capability. + """ + + model_config = ConfigDict( + extra="allow", + ) diff --git a/src/ucp_sdk/models/schemas/shopping/discount_create_request.py b/src/ucp_sdk/models/schemas/shopping/discount_create_request.py new file mode 100644 index 0000000..8e124ee --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/discount_create_request.py @@ -0,0 +1,138 @@ +# 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 .cart_create_request import CartCreateRequest +from .checkout_create_request import CheckoutCreateRequest +from .types import amount_create_request, reverse_domain_name_create_request + +DiscountExtensionCreateRequest = TypeAliasType( + "DiscountExtensionCreateRequest", + Annotated[Any, Field(..., title="Discount Extension Create Request")], +) +""" +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 + """ + JSONPath to the allocation target (e.g., '$.line_items[0]', '$.totals.shipping'). + """ + amount: amount_create_request.AmountCreateRequest + """ + Amount allocated to this target in ISO 4217 minor units. + """ + + +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. + """ + + +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_create_request.AmountCreateRequest + """ + 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_create_request.ReverseDomainNameCreateRequest | 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 Cart(CartCreateRequest): + """ + Cart extended with discount capability. + """ + + model_config = ConfigDict( + extra="allow", + ) + discounts: DiscountsObject | None = None + + +class Checkout(CheckoutCreateRequest): + """ + Checkout extended with discount capability. + """ + + model_config = ConfigDict( + extra="allow", + ) + discounts: DiscountsObject | None = None diff --git a/src/ucp_sdk/models/schemas/shopping/discount_update_request.py b/src/ucp_sdk/models/schemas/shopping/discount_update_request.py new file mode 100644 index 0000000..106f674 --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/discount_update_request.py @@ -0,0 +1,138 @@ +# 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 .cart_update_request import CartUpdateRequest +from .checkout_update_request import CheckoutUpdateRequest +from .types import amount_update_request, reverse_domain_name_update_request + +DiscountExtensionUpdateRequest = TypeAliasType( + "DiscountExtensionUpdateRequest", + Annotated[Any, Field(..., title="Discount Extension Update Request")], +) +""" +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 + """ + JSONPath to the allocation target (e.g., '$.line_items[0]', '$.totals.shipping'). + """ + amount: amount_update_request.AmountUpdateRequest + """ + Amount allocated to this target in ISO 4217 minor units. + """ + + +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. + """ + + +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_update_request.AmountUpdateRequest + """ + 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_update_request.ReverseDomainNameUpdateRequest | 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 Cart(CartUpdateRequest): + """ + Cart extended with discount capability. + """ + + model_config = ConfigDict( + extra="allow", + ) + discounts: DiscountsObject | None = None + + +class Checkout(CheckoutUpdateRequest): + """ + Checkout extended with discount capability. + """ + + model_config = ConfigDict( + extra="allow", + ) + discounts: DiscountsObject | None = None diff --git a/src/ucp_sdk/models/schemas/shopping/fulfillment_complete_request.py b/src/ucp_sdk/models/schemas/shopping/fulfillment_complete_request.py new file mode 100644 index 0000000..42d7872 --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/fulfillment_complete_request.py @@ -0,0 +1,83 @@ +# 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 +from typing_extensions import TypeAliasType + +from .checkout_complete_request import CheckoutCompleteRequest +from .types import ( + fulfillment_available_method_complete_request, + fulfillment_complete_request, + fulfillment_group_complete_request, + fulfillment_method_complete_request, + fulfillment_option_complete_request, +) + +FulfillmentExtensionCompleteRequest = TypeAliasType( + "FulfillmentExtensionCompleteRequest", + Annotated[Any, Field(..., title="Fulfillment Extension Complete Request")], +) +""" +Extends Checkout with fulfillment support using methods, destinations, and groups. +""" + + +FulfillmentAvailableMethod = TypeAliasType( + "FulfillmentAvailableMethod", + fulfillment_available_method_complete_request.FulfillmentAvailableMethodCompleteRequest, +) + + +DevUcpShoppingFulfillment = TypeAliasType("DevUcpShoppingFulfillment", Any) + + +FulfillmentOption = TypeAliasType( + "FulfillmentOption", + fulfillment_option_complete_request.FulfillmentOptionCompleteRequest, +) + + +FulfillmentGroup = TypeAliasType( + "FulfillmentGroup", + fulfillment_group_complete_request.FulfillmentGroupCompleteRequest, +) + + +FulfillmentMethod = TypeAliasType( + "FulfillmentMethod", + fulfillment_method_complete_request.FulfillmentMethodCompleteRequest, +) + + +Fulfillment = TypeAliasType( + "Fulfillment", fulfillment_complete_request.FulfillmentCompleteRequest +) + + +class Checkout(CheckoutCompleteRequest): + """ + Checkout extended with hierarchical fulfillment. + """ + + model_config = ConfigDict( + extra="allow", + ) diff --git a/src/ucp_sdk/models/schemas/shopping/fulfillment_create_request.py b/src/ucp_sdk/models/schemas/shopping/fulfillment_create_request.py new file mode 100644 index 0000000..d4de39b --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/fulfillment_create_request.py @@ -0,0 +1,87 @@ +# 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 +from typing_extensions import TypeAliasType + +from .checkout_create_request import CheckoutCreateRequest +from .types import ( + fulfillment_available_method_create_request, + fulfillment_create_request, + fulfillment_group_create_request, + fulfillment_method_create_request, + fulfillment_option_create_request, +) + +FulfillmentExtensionCreateRequest = TypeAliasType( + "FulfillmentExtensionCreateRequest", + Annotated[Any, Field(..., title="Fulfillment Extension Create Request")], +) +""" +Extends Checkout with fulfillment support using methods, destinations, and groups. +""" + + +FulfillmentAvailableMethod = TypeAliasType( + "FulfillmentAvailableMethod", + fulfillment_available_method_create_request.FulfillmentAvailableMethodCreateRequest, +) + + +DevUcpShoppingFulfillment = TypeAliasType("DevUcpShoppingFulfillment", Any) + + +FulfillmentOption = TypeAliasType( + "FulfillmentOption", + fulfillment_option_create_request.FulfillmentOptionCreateRequest, +) + + +FulfillmentGroup = TypeAliasType( + "FulfillmentGroup", + fulfillment_group_create_request.FulfillmentGroupCreateRequest, +) + + +FulfillmentMethod = TypeAliasType( + "FulfillmentMethod", + fulfillment_method_create_request.FulfillmentMethodCreateRequest, +) + + +Fulfillment = TypeAliasType( + "Fulfillment", fulfillment_create_request.FulfillmentCreateRequest +) + + +class Checkout(CheckoutCreateRequest): + """ + Checkout extended with hierarchical fulfillment. + """ + + model_config = ConfigDict( + extra="allow", + ) + fulfillment: Fulfillment | None = None + """ + Fulfillment details. + """ diff --git a/src/ucp_sdk/models/schemas/shopping/fulfillment_update_request.py b/src/ucp_sdk/models/schemas/shopping/fulfillment_update_request.py new file mode 100644 index 0000000..30265a6 --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/fulfillment_update_request.py @@ -0,0 +1,87 @@ +# 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 +from typing_extensions import TypeAliasType + +from .checkout_update_request import CheckoutUpdateRequest +from .types import ( + fulfillment_available_method_update_request, + fulfillment_group_update_request, + fulfillment_method_update_request, + fulfillment_option_update_request, + fulfillment_update_request, +) + +FulfillmentExtensionUpdateRequest = TypeAliasType( + "FulfillmentExtensionUpdateRequest", + Annotated[Any, Field(..., title="Fulfillment Extension Update Request")], +) +""" +Extends Checkout with fulfillment support using methods, destinations, and groups. +""" + + +FulfillmentOption = TypeAliasType( + "FulfillmentOption", + fulfillment_option_update_request.FulfillmentOptionUpdateRequest, +) + + +FulfillmentGroup = TypeAliasType( + "FulfillmentGroup", + fulfillment_group_update_request.FulfillmentGroupUpdateRequest, +) + + +FulfillmentAvailableMethod = TypeAliasType( + "FulfillmentAvailableMethod", + fulfillment_available_method_update_request.FulfillmentAvailableMethodUpdateRequest, +) + + +DevUcpShoppingFulfillment = TypeAliasType("DevUcpShoppingFulfillment", Any) + + +FulfillmentMethod = TypeAliasType( + "FulfillmentMethod", + fulfillment_method_update_request.FulfillmentMethodUpdateRequest, +) + + +Fulfillment = TypeAliasType( + "Fulfillment", fulfillment_update_request.FulfillmentUpdateRequest +) + + +class Checkout(CheckoutUpdateRequest): + """ + Checkout extended with hierarchical fulfillment. + """ + + model_config = ConfigDict( + extra="allow", + ) + fulfillment: Fulfillment | None = None + """ + Fulfillment details. + """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/__init__.py b/src/ucp_sdk/models/schemas/shopping/types/__init__.py index 1252d6b..421dc21 100644 --- a/src/ucp_sdk/models/schemas/shopping/types/__init__.py +++ b/src/ucp_sdk/models/schemas/shopping/types/__init__.py @@ -15,3 +15,4 @@ # generated by datamodel-codegen # pylint: disable=all # pyformat: disable + diff --git a/src/ucp_sdk/models/schemas/shopping/types/amount_complete_request.py b/src/ucp_sdk/models/schemas/shopping/types/amount_complete_request.py new file mode 100644 index 0000000..1ab0374 --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/types/amount_complete_request.py @@ -0,0 +1,32 @@ +# 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 + +AmountCompleteRequest = TypeAliasType( + "AmountCompleteRequest", + Annotated[int, Field(..., ge=0, title="Amount Complete 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). +""" diff --git a/src/ucp_sdk/models/schemas/shopping/types/amount_create_request.py b/src/ucp_sdk/models/schemas/shopping/types/amount_create_request.py new file mode 100644 index 0000000..b996539 --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/types/amount_create_request.py @@ -0,0 +1,32 @@ +# 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 + +AmountCreateRequest = TypeAliasType( + "AmountCreateRequest", + Annotated[int, Field(..., ge=0, title="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). +""" diff --git a/src/ucp_sdk/models/schemas/shopping/types/amount_update_request.py b/src/ucp_sdk/models/schemas/shopping/types/amount_update_request.py new file mode 100644 index 0000000..eb465c0 --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/types/amount_update_request.py @@ -0,0 +1,32 @@ +# 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 + +AmountUpdateRequest = TypeAliasType( + "AmountUpdateRequest", + Annotated[int, Field(..., ge=0, title="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). +""" diff --git a/src/ucp_sdk/models/schemas/shopping/types/business_fulfillment_config_complete_request.py b/src/ucp_sdk/models/schemas/shopping/types/business_fulfillment_config_complete_request.py new file mode 100644 index 0000000..4ec3c71 --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/types/business_fulfillment_config_complete_request.py @@ -0,0 +1,61 @@ +# 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 AllowsMultiDestination(BaseModel): + """ + Permits multiple destinations per method type. + """ + + model_config = ConfigDict( + extra="forbid", + ) + shipping: bool | None = None + """ + Multiple shipping destinations allowed. + """ + pickup: bool | None = None + """ + Multiple pickup locations allowed. + """ + + +class BusinessFulfillmentConfigCompleteRequest(BaseModel): + """ + Business's fulfillment configuration. + """ + + model_config = ConfigDict( + extra="allow", + ) + allows_multi_destination: AllowsMultiDestination | None = None + """ + Permits multiple destinations per method type. + """ + allows_method_combinations: ( + list[list[Literal["shipping", "pickup"]]] | None + ) = None + """ + Allowed method type combinations. + """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/business_fulfillment_config_create_request.py b/src/ucp_sdk/models/schemas/shopping/types/business_fulfillment_config_create_request.py new file mode 100644 index 0000000..fd9c73e --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/types/business_fulfillment_config_create_request.py @@ -0,0 +1,61 @@ +# 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 AllowsMultiDestination(BaseModel): + """ + Permits multiple destinations per method type. + """ + + model_config = ConfigDict( + extra="forbid", + ) + shipping: bool | None = None + """ + Multiple shipping destinations allowed. + """ + pickup: bool | None = None + """ + Multiple pickup locations allowed. + """ + + +class BusinessFulfillmentConfigCreateRequest(BaseModel): + """ + Business's fulfillment configuration. + """ + + model_config = ConfigDict( + extra="allow", + ) + allows_multi_destination: AllowsMultiDestination | None = None + """ + Permits multiple destinations per method type. + """ + allows_method_combinations: ( + list[list[Literal["shipping", "pickup"]]] | None + ) = None + """ + Allowed method type combinations. + """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/business_fulfillment_config_update_request.py b/src/ucp_sdk/models/schemas/shopping/types/business_fulfillment_config_update_request.py new file mode 100644 index 0000000..a3e62dc --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/types/business_fulfillment_config_update_request.py @@ -0,0 +1,61 @@ +# 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 AllowsMultiDestination(BaseModel): + """ + Permits multiple destinations per method type. + """ + + model_config = ConfigDict( + extra="forbid", + ) + shipping: bool | None = None + """ + Multiple shipping destinations allowed. + """ + pickup: bool | None = None + """ + Multiple pickup locations allowed. + """ + + +class BusinessFulfillmentConfigUpdateRequest(BaseModel): + """ + Business's fulfillment configuration. + """ + + model_config = ConfigDict( + extra="allow", + ) + allows_multi_destination: AllowsMultiDestination | None = None + """ + Permits multiple destinations per method type. + """ + allows_method_combinations: ( + list[list[Literal["shipping", "pickup"]]] | None + ) = None + """ + Allowed method type combinations. + """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/buyer_complete_request.py b/src/ucp_sdk/models/schemas/shopping/types/buyer_complete_request.py new file mode 100644 index 0000000..0398326 --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/types/buyer_complete_request.py @@ -0,0 +1,43 @@ +# 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 BuyerCompleteRequest(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/fulfillment_available_method_complete_request.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_available_method_complete_request.py new file mode 100644 index 0000000..f04941f --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_available_method_complete_request.py @@ -0,0 +1,31 @@ +# 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 FulfillmentAvailableMethodCompleteRequest(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_complete_request.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_complete_request.py new file mode 100644 index 0000000..ef01ec9 --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_complete_request.py @@ -0,0 +1,42 @@ +# 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_complete_request + + +class FulfillmentCompleteRequest(BaseModel): + """ + Container for fulfillment methods and availability. + """ + + model_config = ConfigDict( + extra="allow", + ) + methods: ( + list[ + fulfillment_method_complete_request.FulfillmentMethodCompleteRequest + ] + | None + ) = None + """ + Fulfillment methods for cart items. + """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_destination_complete_request.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_destination_complete_request.py new file mode 100644 index 0000000..f92a22a --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_destination_complete_request.py @@ -0,0 +1,38 @@ +# 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 retail_location, shipping_destination + +FulfillmentDestinationCompleteRequest = TypeAliasType( + "FulfillmentDestinationCompleteRequest", + Annotated[ + shipping_destination.ShippingDestination + | retail_location.RetailLocation, + Field(..., title="Fulfillment Destination Complete Request"), + ], +) +""" +A destination for fulfillment. +""" 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 index 2eeb290..4d7bbb5 100644 --- 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 @@ -23,13 +23,16 @@ from pydantic import Field from typing_extensions import TypeAliasType -from . import retail_location, shipping_destination +from . import ( + retail_location_create_request, + shipping_destination_create_request, +) FulfillmentDestinationCreateRequest = TypeAliasType( "FulfillmentDestinationCreateRequest", Annotated[ - shipping_destination.ShippingDestination - | retail_location.RetailLocation, + shipping_destination_create_request.ShippingDestinationCreateRequest + | retail_location_create_request.RetailLocationCreateRequest, Field(..., title="Fulfillment Destination Create Request"), ], ) 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 index e04ace3..add2079 100644 --- 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 @@ -23,13 +23,16 @@ from pydantic import Field from typing_extensions import TypeAliasType -from . import retail_location, shipping_destination +from . import ( + retail_location_update_request, + shipping_destination_update_request, +) FulfillmentDestinationUpdateRequest = TypeAliasType( "FulfillmentDestinationUpdateRequest", Annotated[ - shipping_destination.ShippingDestination - | retail_location.RetailLocation, + shipping_destination_update_request.ShippingDestinationUpdateRequest + | retail_location_update_request.RetailLocationUpdateRequest, Field(..., title="Fulfillment Destination Update Request"), ], ) diff --git a/src/ucp_sdk/models/schemas/shopping/types/fulfillment_group_complete_request.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_group_complete_request.py new file mode 100644 index 0000000..228e850 --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_group_complete_request.py @@ -0,0 +1,35 @@ +# 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 FulfillmentGroupCompleteRequest(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_method_complete_request.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_method_complete_request.py new file mode 100644 index 0000000..ba6a8a0 --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_method_complete_request.py @@ -0,0 +1,56 @@ +# 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_destination_complete_request, + fulfillment_group_complete_request, +) + + +class FulfillmentMethodCompleteRequest(BaseModel): + """ + A fulfillment method (shipping or pickup) with destinations and groups. + """ + + model_config = ConfigDict( + extra="allow", + ) + destinations: ( + list[ + fulfillment_destination_complete_request.FulfillmentDestinationCompleteRequest + ] + | None + ) = None + """ + Available destinations. For shipping: addresses. For pickup: retail locations. + """ + selected_destination_id: str | None = None + """ + ID of the selected destination. + """ + groups: ( + list[fulfillment_group_complete_request.FulfillmentGroupCompleteRequest] + | 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_complete_request.py b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_option_complete_request.py new file mode 100644 index 0000000..87da6d2 --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/types/fulfillment_option_complete_request.py @@ -0,0 +1,31 @@ +# 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 FulfillmentOptionCompleteRequest(BaseModel): + """ + A fulfillment option within a group (e.g., Standard Shipping $5, Express $15). + """ + + model_config = ConfigDict( + extra="allow", + ) diff --git a/src/ucp_sdk/models/schemas/shopping/types/platform_fulfillment_config_complete_request.py b/src/ucp_sdk/models/schemas/shopping/types/platform_fulfillment_config_complete_request.py new file mode 100644 index 0000000..8c5ac73 --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/types/platform_fulfillment_config_complete_request.py @@ -0,0 +1,35 @@ +# 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 PlatformFulfillmentConfigCompleteRequest(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/platform_fulfillment_config_create_request.py b/src/ucp_sdk/models/schemas/shopping/types/platform_fulfillment_config_create_request.py new file mode 100644 index 0000000..4ef8e40 --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/types/platform_fulfillment_config_create_request.py @@ -0,0 +1,35 @@ +# 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 PlatformFulfillmentConfigCreateRequest(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/platform_fulfillment_config_update_request.py b/src/ucp_sdk/models/schemas/shopping/types/platform_fulfillment_config_update_request.py new file mode 100644 index 0000000..1a656fe --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/types/platform_fulfillment_config_update_request.py @@ -0,0 +1,35 @@ +# 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 PlatformFulfillmentConfigUpdateRequest(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/reverse_domain_name_complete_request.py b/src/ucp_sdk/models/schemas/shopping/types/reverse_domain_name_complete_request.py new file mode 100644 index 0000000..c05134e --- /dev/null +++ b/src/ucp_sdk/models/schemas/shopping/types/reverse_domain_name_complete_request.py @@ -0,0 +1,39 @@ +# 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 + +ReverseDomainNameCompleteRequest = TypeAliasType( + "ReverseDomainNameCompleteRequest", + Annotated[ + str, + Field( + ..., + pattern="^[a-z][a-z0-9]*(?:\\.[a-z][a-z0-9_]*)+$", + title="Reverse Domain Name Complete 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'). +""" 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 index 46cfc64..b454152 100644 --- 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 @@ -20,10 +20,10 @@ from pydantic import ConfigDict -from .postal_address import PostalAddress +from .postal_address_create_request import PostalAddressCreateRequest -class ShippingDestinationCreateRequest(PostalAddress): +class ShippingDestinationCreateRequest(PostalAddressCreateRequest): """ Shipping destination. """ 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 index fea30e9..3ab1733 100644 --- 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 @@ -20,10 +20,10 @@ from pydantic import ConfigDict -from .postal_address import PostalAddress +from .postal_address_update_request import PostalAddressUpdateRequest -class ShippingDestinationUpdateRequest(PostalAddress): +class ShippingDestinationUpdateRequest(PostalAddressUpdateRequest): """ Shipping destination. """ diff --git a/src/ucp_sdk/models/schemas/shopping/types/totals_create_request.py b/src/ucp_sdk/models/schemas/shopping/types/totals_create_request.py index bb78c2d..e2f89df 100644 --- a/src/ucp_sdk/models/schemas/shopping/types/totals_create_request.py +++ b/src/ucp_sdk/models/schemas/shopping/types/totals_create_request.py @@ -23,7 +23,7 @@ from pydantic import Field, AfterValidator from typing_extensions import TypeAliasType -from . import total +from . import total_create_request def _enforce_contains_totals_create_request(value): @@ -98,7 +98,7 @@ def _enforce_contains_totals_create_request(value): TotalsCreateRequest = TypeAliasType( "TotalsCreateRequest", Annotated[ - list[total.Total], + list[total_create_request.TotalCreateRequest], Field(..., title="Totals Create Request"), AfterValidator(_enforce_contains_totals_create_request), ], diff --git a/src/ucp_sdk/models/schemas/shopping/types/totals_update_request.py b/src/ucp_sdk/models/schemas/shopping/types/totals_update_request.py index 9559f7d..15eba47 100644 --- a/src/ucp_sdk/models/schemas/shopping/types/totals_update_request.py +++ b/src/ucp_sdk/models/schemas/shopping/types/totals_update_request.py @@ -23,7 +23,7 @@ from pydantic import Field, AfterValidator from typing_extensions import TypeAliasType -from . import total +from . import total_update_request def _enforce_contains_totals_update_request(value): @@ -98,7 +98,7 @@ def _enforce_contains_totals_update_request(value): TotalsUpdateRequest = TypeAliasType( "TotalsUpdateRequest", Annotated[ - list[total.Total], + list[total_update_request.TotalUpdateRequest], Field(..., title="Totals Update Request"), AfterValidator(_enforce_contains_totals_update_request), ], diff --git a/src/ucp_sdk/models/schemas/transports/__init__.py b/src/ucp_sdk/models/schemas/transports/__init__.py index 1252d6b..421dc21 100644 --- a/src/ucp_sdk/models/schemas/transports/__init__.py +++ b/src/ucp_sdk/models/schemas/transports/__init__.py @@ -15,3 +15,4 @@ # generated by datamodel-codegen # pylint: disable=all # pyformat: disable + diff --git a/src/ucp_sdk/models/schemas/ucp.py b/src/ucp_sdk/models/schemas/ucp.py index 373f7a9..28b2a71 100644 --- a/src/ucp_sdk/models/schemas/ucp.py +++ b/src/ucp_sdk/models/schemas/ucp.py @@ -174,7 +174,7 @@ class PlatformSchema(Base): extra="allow", ) services: dict[ - reverse_domain_name.ReverseDomainName, list[service.PlatformSchema5] + reverse_domain_name.ReverseDomainName, list[service.PlatformSchema8] ] """ Service registry keyed by reverse-domain name. @@ -211,7 +211,7 @@ class BusinessSchema(Base): 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.BusinessSchema2] + reverse_domain_name.ReverseDomainName, list[service.BusinessSchema5] ] """ Service registry keyed by reverse-domain name. @@ -245,7 +245,7 @@ class ResponseCheckoutSchema(Base): ) services: ( dict[ - reverse_domain_name.ReverseDomainName, list[service.ResponseSchema2] + reverse_domain_name.ReverseDomainName, list[service.ResponseSchema5] ] | None ) = None diff --git a/src/ucp_sdk/models/schemas/ucp_create_request.py b/src/ucp_sdk/models/schemas/ucp_create_request.py index 830d878..8fdb55e 100644 --- a/src/ucp_sdk/models/schemas/ucp_create_request.py +++ b/src/ucp_sdk/models/schemas/ucp_create_request.py @@ -23,8 +23,8 @@ from pydantic import AnyUrl, BaseModel, ConfigDict, Field from typing_extensions import TypeAliasType -from . import capability, payment_handler, service -from .shopping.types import reverse_domain_name +from . import capability_create_request, payment_handler, service +from .shopping.types import reverse_domain_name_create_request Version = TypeAliasType( "Version", Annotated[str, Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$")] @@ -65,7 +65,11 @@ class Requires(BaseModel): Required protocol version. """ capabilities: ( - dict[reverse_domain_name.ReverseDomainName, VersionConstraint] | None + 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. @@ -116,20 +120,30 @@ class Base(BaseModel): Application-level status of the UCP operation. """ services: ( - dict[reverse_domain_name.ReverseDomainName, list[service.Base]] | None + dict[ + reverse_domain_name_create_request.ReverseDomainNameCreateRequest, + list[service.Base], + ] + | None ) = None """ Service registry keyed by reverse-domain name. """ capabilities: ( - dict[reverse_domain_name.ReverseDomainName, list[capability.Base]] + dict[ + reverse_domain_name_create_request.ReverseDomainNameCreateRequest, + list[capability_create_request.Base], + ] | None ) = None """ Capability registry keyed by reverse-domain name. """ payment_handlers: ( - dict[reverse_domain_name.ReverseDomainName, list[payment_handler.Base]] + dict[ + reverse_domain_name_create_request.ReverseDomainNameCreateRequest, + list[payment_handler.Base], + ] | None ) = None """ @@ -174,15 +188,16 @@ class PlatformSchema(Base): extra="allow", ) services: dict[ - reverse_domain_name.ReverseDomainName, list[service.PlatformSchema5] + reverse_domain_name_create_request.ReverseDomainNameCreateRequest, + list[service.PlatformSchema8], ] """ Service registry keyed by reverse-domain name. """ capabilities: ( dict[ - reverse_domain_name.ReverseDomainName, - list[capability.PlatformSchema], + reverse_domain_name_create_request.ReverseDomainNameCreateRequest, + list[capability_create_request.PlatformSchema], ] | None ) = None @@ -190,7 +205,7 @@ class PlatformSchema(Base): Capability registry keyed by reverse-domain name. """ payment_handlers: dict[ - reverse_domain_name.ReverseDomainName, + reverse_domain_name_create_request.ReverseDomainNameCreateRequest, list[payment_handler.PlatformSchema], ] """ @@ -211,15 +226,16 @@ class BusinessSchema(Base): 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.BusinessSchema2] + reverse_domain_name_create_request.ReverseDomainNameCreateRequest, + list[service.BusinessSchema5], ] """ Service registry keyed by reverse-domain name. """ capabilities: ( dict[ - reverse_domain_name.ReverseDomainName, - list[capability.BusinessSchema], + reverse_domain_name_create_request.ReverseDomainNameCreateRequest, + list[capability_create_request.BusinessSchema], ] | None ) = None @@ -227,7 +243,7 @@ class BusinessSchema(Base): Capability registry keyed by reverse-domain name. """ payment_handlers: dict[ - reverse_domain_name.ReverseDomainName, + reverse_domain_name_create_request.ReverseDomainNameCreateRequest, list[payment_handler.BusinessSchema], ] """ @@ -245,7 +261,8 @@ class ResponseCheckoutSchema(Base): ) services: ( dict[ - reverse_domain_name.ReverseDomainName, list[service.ResponseSchema2] + reverse_domain_name_create_request.ReverseDomainNameCreateRequest, + list[service.ResponseSchema5], ] | None ) = None @@ -254,8 +271,8 @@ class ResponseCheckoutSchema(Base): """ capabilities: ( dict[ - reverse_domain_name.ReverseDomainName, - list[capability.ResponseSchema], + reverse_domain_name_create_request.ReverseDomainNameCreateRequest, + list[capability_create_request.ResponseSchema], ] | None ) = None @@ -263,7 +280,7 @@ class ResponseCheckoutSchema(Base): Capability registry keyed by reverse-domain name. """ payment_handlers: dict[ - reverse_domain_name.ReverseDomainName, + reverse_domain_name_create_request.ReverseDomainNameCreateRequest, list[payment_handler.ResponseSchema], ] """ @@ -281,8 +298,8 @@ class ResponseOrderSchema(Base): ) capabilities: ( dict[ - reverse_domain_name.ReverseDomainName, - list[capability.ResponseSchema], + reverse_domain_name_create_request.ReverseDomainNameCreateRequest, + list[capability_create_request.ResponseSchema], ] | None ) = None @@ -301,8 +318,8 @@ class ResponseCartSchema(Base): ) capabilities: ( dict[ - reverse_domain_name.ReverseDomainName, - list[capability.ResponseSchema], + reverse_domain_name_create_request.ReverseDomainNameCreateRequest, + list[capability_create_request.ResponseSchema], ] | None ) = None @@ -321,8 +338,8 @@ class ResponseCatalogSchema(Base): ) capabilities: ( dict[ - reverse_domain_name.ReverseDomainName, - list[capability.ResponseSchema], + reverse_domain_name_create_request.ReverseDomainNameCreateRequest, + list[capability_create_request.ResponseSchema], ] | None ) = None diff --git a/src/ucp_sdk/models/schemas/ucp_update_request.py b/src/ucp_sdk/models/schemas/ucp_update_request.py index 423f664..256eaff 100644 --- a/src/ucp_sdk/models/schemas/ucp_update_request.py +++ b/src/ucp_sdk/models/schemas/ucp_update_request.py @@ -23,8 +23,8 @@ from pydantic import AnyUrl, BaseModel, ConfigDict, Field from typing_extensions import TypeAliasType -from . import capability, payment_handler, service -from .shopping.types import reverse_domain_name +from . import capability_update_request, payment_handler, service +from .shopping.types import reverse_domain_name_update_request Version = TypeAliasType( "Version", Annotated[str, Field(..., pattern="^\\d{4}-\\d{2}-\\d{2}$")] @@ -65,7 +65,11 @@ class Requires(BaseModel): Required protocol version. """ capabilities: ( - dict[reverse_domain_name.ReverseDomainName, VersionConstraint] | None + 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. @@ -116,20 +120,30 @@ class Base(BaseModel): Application-level status of the UCP operation. """ services: ( - dict[reverse_domain_name.ReverseDomainName, list[service.Base]] | None + dict[ + reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, + list[service.Base], + ] + | None ) = None """ Service registry keyed by reverse-domain name. """ capabilities: ( - dict[reverse_domain_name.ReverseDomainName, list[capability.Base]] + dict[ + reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, + list[capability_update_request.Base], + ] | None ) = None """ Capability registry keyed by reverse-domain name. """ payment_handlers: ( - dict[reverse_domain_name.ReverseDomainName, list[payment_handler.Base]] + dict[ + reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, + list[payment_handler.Base], + ] | None ) = None """ @@ -174,15 +188,16 @@ class PlatformSchema(Base): extra="allow", ) services: dict[ - reverse_domain_name.ReverseDomainName, list[service.PlatformSchema5] + reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, + list[service.PlatformSchema8], ] """ Service registry keyed by reverse-domain name. """ capabilities: ( dict[ - reverse_domain_name.ReverseDomainName, - list[capability.PlatformSchema], + reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, + list[capability_update_request.PlatformSchema], ] | None ) = None @@ -190,7 +205,7 @@ class PlatformSchema(Base): Capability registry keyed by reverse-domain name. """ payment_handlers: dict[ - reverse_domain_name.ReverseDomainName, + reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, list[payment_handler.PlatformSchema], ] """ @@ -211,15 +226,16 @@ class BusinessSchema(Base): 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.BusinessSchema2] + reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, + list[service.BusinessSchema5], ] """ Service registry keyed by reverse-domain name. """ capabilities: ( dict[ - reverse_domain_name.ReverseDomainName, - list[capability.BusinessSchema], + reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, + list[capability_update_request.BusinessSchema], ] | None ) = None @@ -227,7 +243,7 @@ class BusinessSchema(Base): Capability registry keyed by reverse-domain name. """ payment_handlers: dict[ - reverse_domain_name.ReverseDomainName, + reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, list[payment_handler.BusinessSchema], ] """ @@ -245,7 +261,8 @@ class ResponseCheckoutSchema(Base): ) services: ( dict[ - reverse_domain_name.ReverseDomainName, list[service.ResponseSchema2] + reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, + list[service.ResponseSchema5], ] | None ) = None @@ -254,8 +271,8 @@ class ResponseCheckoutSchema(Base): """ capabilities: ( dict[ - reverse_domain_name.ReverseDomainName, - list[capability.ResponseSchema], + reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, + list[capability_update_request.ResponseSchema], ] | None ) = None @@ -263,7 +280,7 @@ class ResponseCheckoutSchema(Base): Capability registry keyed by reverse-domain name. """ payment_handlers: dict[ - reverse_domain_name.ReverseDomainName, + reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, list[payment_handler.ResponseSchema], ] """ @@ -281,8 +298,8 @@ class ResponseOrderSchema(Base): ) capabilities: ( dict[ - reverse_domain_name.ReverseDomainName, - list[capability.ResponseSchema], + reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, + list[capability_update_request.ResponseSchema], ] | None ) = None @@ -301,8 +318,8 @@ class ResponseCartSchema(Base): ) capabilities: ( dict[ - reverse_domain_name.ReverseDomainName, - list[capability.ResponseSchema], + reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, + list[capability_update_request.ResponseSchema], ] | None ) = None @@ -321,8 +338,8 @@ class ResponseCatalogSchema(Base): ) capabilities: ( dict[ - reverse_domain_name.ReverseDomainName, - list[capability.ResponseSchema], + reverse_domain_name_update_request.ReverseDomainNameUpdateRequest, + list[capability_update_request.ResponseSchema], ] | None ) = None diff --git a/tests/test_codegen_pipeline.py b/tests/test_codegen_pipeline.py index a6dd3ab..3e693de 100644 --- a/tests/test_codegen_pipeline.py +++ b/tests/test_codegen_pipeline.py @@ -218,6 +218,31 @@ def test_get_required_ops_collects_all_declared_operations(self) -> None: {"create", "update", "complete"}, ) + def test_get_required_ops_collects_from_nested_defs(self) -> None: + """Markers inside $defs contribute their operations even if root has none.""" + schema = { + "title": "Extension", + "type": "object", + "$defs": { + "custom": { + "type": "object", + "properties": { + "secret": {"ucp_request": "omit"}, + "action": { + "ucp_request": { + "complete": "optional", + } + }, + }, + } + }, + } + + 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 = [ @@ -456,6 +481,94 @@ def test_generate_variants_writes_operation_specific_files(self) -> None: self.assertEqual(set(update_variant["properties"]), {"id"}) self.assertEqual(update_variant["required"], ["id"]) + def test_nested_defs_filtered_and_refs_rewritten(self) -> None: + """Nested $defs properties are filtered and external refs rewritten.""" + schema = { + "$id": "https://ucp.dev/schemas/food/cart.json", + "title": "Cart", + "type": "object", + "$defs": { + "checkout": { + "title": "Checkout with Cart", + "type": "object", + "properties": { + "cart_id": { + "type": "string", + "ucp_request": { + "create": "optional", + "update": "omit", + "complete": "omit", + }, + } + }, + "allOf": [{"$ref": "checkout.json"}], + } + }, + "properties": { + "restaurant": {"$ref": "restaurant.json"}, + }, + } + file_path = Path("/schemas/food/cart.json") + checkout_path = str((file_path.parent / "checkout.json").resolve()) + restaurant_path = str((file_path.parent / "restaurant.json").resolve()) + variant_needs = { + checkout_path: {"create", "update", "complete"}, + restaurant_path: {"create", "update"}, + } + + create_variant = preprocess_schemas._create_single_variant( + schema, + "create", + "cart", + file_path, + variant_needs, + ) + update_variant = preprocess_schemas._create_single_variant( + schema, + "update", + "cart", + file_path, + variant_needs, + ) + complete_variant = preprocess_schemas._create_single_variant( + schema, + "complete", + "cart", + file_path, + variant_needs, + ) + + # Create variant includes cart_id (without ucp_request) and rewrites checkout.json + checkout_def_create = create_variant["$defs"]["checkout"] + self.assertIn("cart_id", checkout_def_create["properties"]) + self.assertNotIn( + "ucp_request", checkout_def_create["properties"]["cart_id"] + ) + self.assertEqual( + checkout_def_create["allOf"][0]["$ref"], + "checkout_create_request.json", + ) + self.assertEqual( + create_variant["properties"]["restaurant"]["$ref"], + "restaurant_create_request.json", + ) + + # Update variant omits cart_id and rewrites checkout.json + checkout_def_update = update_variant["$defs"]["checkout"] + self.assertEqual(checkout_def_update["properties"], {}) + self.assertEqual( + checkout_def_update["allOf"][0]["$ref"], + "checkout_update_request.json", + ) + + # Complete variant omits cart_id and rewrites checkout.json + checkout_def_complete = complete_variant["$defs"]["checkout"] + self.assertEqual(checkout_def_complete["properties"], {}) + self.assertEqual( + checkout_def_complete["allOf"][0]["$ref"], + "checkout_complete_request.json", + ) + class PipelineDependencyTest(unittest.TestCase): """Tests metadata normalization and transitive variant dependencies.""" @@ -717,6 +830,123 @@ def test_propagation_with_fragment(self) -> None: "child_create_request.json#/$defs/item", ) + def test_main_preprocesses_nested_capability_extensions_end_to_end( + self, + ) -> None: + """Capability extensions in $defs trigger variants and propagate refs.""" + 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/checkout.json", + "title": "Checkout", + "type": "object", + "properties": { + "id": { + "type": "string", + "ucp_request": { + "create": "omit", + "update": "required", + }, + } + }, + }, + root / "checkout.json", + ) + preprocess_schemas.save_json( + { + "$id": "https://ucp.dev/schemas/cart.json", + "title": "Cart", + "type": "object", + "$defs": { + "checkout": { + "title": "Checkout with Cart", + "type": "object", + "properties": { + "cart_id": { + "type": "string", + "ucp_request": { + "create": "optional", + "update": "omit", + }, + } + }, + "allOf": [{"$ref": "checkout.json"}], + } + }, + "properties": { + "items": {"type": "array"}, + }, + }, + root / "cart.json", + ) + + with ( + mock.patch.object( + sys, + "argv", + ["preprocess_schemas.py", str(root)], + ), + contextlib.redirect_stdout(io.StringIO()), + ): + preprocess_schemas.main() + + self.assertTrue( + (root / "checkout_create_request.json").exists(), + "checkout_create_request.json was not generated", + ) + self.assertTrue( + (root / "checkout_update_request.json").exists(), + "checkout_update_request.json was not generated", + ) + self.assertTrue( + (root / "cart_create_request.json").exists(), + "cart_create_request.json was not generated", + ) + self.assertTrue( + (root / "cart_update_request.json").exists(), + "cart_update_request.json was not generated", + ) + + cart_create = preprocess_schemas.load_json( + root / "cart_create_request.json" + ) + cart_update = preprocess_schemas.load_json( + root / "cart_update_request.json" + ) + + # Check cart_create_request.json + self.assertIn( + "cart_id", cart_create["$defs"]["checkout"]["properties"] + ) + self.assertNotIn( + "ucp_request", + cart_create["$defs"]["checkout"]["properties"]["cart_id"], + ) + self.assertEqual( + cart_create["$defs"]["checkout"]["allOf"][0]["$ref"], + "checkout_create_request.json", + ) + + # Check cart_update_request.json + self.assertEqual(cart_update["$defs"]["checkout"]["properties"], {}) + self.assertEqual( + cart_update["$defs"]["checkout"]["allOf"][0]["$ref"], + "checkout_update_request.json", + ) + class MetadataUnionTest(unittest.TestCase): """The UcpMetadata root union is derived from ucp.json $defs.""" @@ -1847,5 +2077,185 @@ def test_sibling_config_keeps_extra_allow(self) -> None: self.assertEqual(config.model_extra, {"bogus": "x"}) +@unittest.skipUnless( + HAVE_SDK, "requires the installed package (pip install -e .)" +) +class CapabilityExtensionSemanticTest(unittest.TestCase): + """Test capability extension models generated across shopping operations.""" + + def test_cart_checkout_create_request_with_cart_id(self) -> None: + """CheckoutCreateRequest with cart capability accepts cart_id.""" + from ucp_sdk.models.schemas.shopping.cart_create_request import ( + Checkout as CartCheckoutCreateRequest, + ) + + req = CartCheckoutCreateRequest(cart_id="cart_12345") + self.assertEqual(req.cart_id, "cart_12345") + self.assertIsNone(req.line_items) + + def test_cart_checkout_create_request_with_line_items(self) -> None: + """CheckoutCreateRequest with cart capability accepts line_items.""" + from ucp_sdk.models.schemas.shopping.cart_create_request import ( + Checkout as CartCheckoutCreateRequest, + ) + + req = CartCheckoutCreateRequest( + line_items=[ + { + "item": { + "id": "prod_1", + "title": "Test Product", + "price": 1000, + }, + "quantity": 1, + } + ] + ) + self.assertIsNotNone(req.line_items) + self.assertIsNone(req.cart_id) + + def test_cart_checkout_create_request_requires_cart_id_or_line_items( + self, + ) -> None: + """CheckoutCreateRequest with cart capability requires cart_id or line_items.""" + from ucp_sdk.models.schemas.shopping.cart_create_request import ( + Checkout as CartCheckoutCreateRequest, + ) + + with self.assertRaises(ValidationError) as ctx: + CartCheckoutCreateRequest() + self.assertIn( + "Either cart_id or line_items must be provided", str(ctx.exception) + ) + + def test_fulfillment_checkout_create_and_update(self) -> None: + """Fulfillment extension models include fulfillment in create and update.""" + from ucp_sdk.models.schemas.shopping.fulfillment_create_request import ( + Checkout as FulfillmentCheckoutCreateRequest, + ) + from ucp_sdk.models.schemas.shopping.fulfillment_update_request import ( + Checkout as FulfillmentCheckoutUpdateRequest, + ) + + create_req = FulfillmentCheckoutCreateRequest( + line_items=[ + { + "item": { + "id": "prod_1", + "title": "Item", + "price": 500, + }, + "quantity": 1, + } + ], + fulfillment={ + "methods": [ + { + "id": "method_1", + "type": "shipping", + "line_item_ids": ["prod_1"], + } + ] + }, + ) + self.assertIsNotNone(create_req.fulfillment) + + update_req = FulfillmentCheckoutUpdateRequest( + line_items=[ + { + "item": { + "id": "prod_1", + "title": "Item", + "price": 500, + }, + "quantity": 1, + } + ], + fulfillment={ + "methods": [ + { + "id": "method_1", + "type": "shipping", + "line_item_ids": ["prod_1"], + "selected_option_id": "opt_ground", + } + ] + }, + ) + self.assertIsNotNone(update_req.fulfillment) + + def test_discount_checkout_and_cart_create_and_update(self) -> None: + """Discount extension models include discounts in create and update.""" + from ucp_sdk.models.schemas.shopping.discount_create_request import ( + Cart as DiscountCartCreateRequest, + Checkout as DiscountCheckoutCreateRequest, + ) + from ucp_sdk.models.schemas.shopping.discount_update_request import ( + Cart as DiscountCartUpdateRequest, + Checkout as DiscountCheckoutUpdateRequest, + ) + + checkout_create = DiscountCheckoutCreateRequest( + line_items=[ + { + "item": { + "id": "prod_1", + "title": "Item", + "price": 500, + }, + "quantity": 1, + } + ], + discounts={"codes": ["SAVE10"]}, + ) + self.assertEqual(checkout_create.discounts.codes, ["SAVE10"]) + + cart_create = DiscountCartCreateRequest( + line_items=[ + { + "item": { + "id": "prod_1", + "title": "Item", + "price": 500, + }, + "quantity": 1, + } + ], + discounts={"codes": ["CART10"]}, + ) + self.assertEqual(cart_create.discounts.codes, ["CART10"]) + + checkout_update = DiscountCheckoutUpdateRequest( + line_items=[ + { + "item": { + "id": "prod_1", + "title": "Item", + "price": 500, + }, + "quantity": 1, + } + ], + discounts={"codes": ["SAVE20"]}, + ) + self.assertEqual(checkout_update.discounts.codes, ["SAVE20"]) + + cart_update = DiscountCartUpdateRequest( + id="cart_123", + line_items=[ + { + "item": { + "id": "prod_1", + "title": "Item", + "price": 500, + }, + "quantity": 1, + } + ], + discounts={"codes": ["CART20"]}, + ) + self.assertEqual(cart_update.discounts.codes, ["CART20"]) + + if __name__ == "__main__": unittest.main()