Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .codespellignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ mor
tok
fpan
dpan
requestor
3 changes: 2 additions & 1 deletion .github/workflows/conventional-commits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: semantic-pull-request
uses: amannn/action-semantic-pull-request@v6
# zizmor: ignore[unpinned-uses]
uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
Expand Down
11 changes: 8 additions & 3 deletions .github/workflows/linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,20 @@ on:
pull_request:
branches: [main]

permissions:
contents: read

jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: '3.x'
- uses: pre-commit/action@v3.0.1
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
env:
# workaround for pre-commit/action not being updated to node24
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
10 changes: 7 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
Expand All @@ -49,14 +51,16 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
- name: Regenerate models from the pinned spec version
# SDK 0.4.x targets UCP 2026-04-08 (see the README compatibility
# SDK 0.5.x targets UCP 2026-08-25 (see the README compatibility
# table). Bump this pin together with the SDK version line.
run: ./generate_models.sh 2026-04-08
run: ./generate_models.sh 2026-08-25
- name: Normalize file endings (as pre-commit's end-of-file-fixer does)
run: |
python3 - <<'PY'
Expand All @@ -72,7 +76,7 @@ jobs:
run: |
git add -A -- src/ucp_sdk/models/schemas
if ! git diff --cached --quiet -- src/ucp_sdk/models/schemas; then
echo "::error::Committed models differ from regeneration against the pinned UCP spec. Either the generation pipeline is broken, or the models were edited without regenerating. Run ./generate_models.sh 2026-04-08 and commit the result."
echo "::error::Committed models differ from regeneration against the pinned UCP spec. Either the generation pipeline is broken, or the models were edited without regenerating. Run ./generate_models.sh 2026-08-25 and commit the result."
git --no-pager diff --cached --stat -- src/ucp_sdk/models/schemas
git --no-pager diff --cached -- src/ucp_sdk/models/schemas
exit 1
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ node_modules/
__pycache__/
.venv/
*.py[cod]
ucp/
uv.lock
23 changes: 13 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ UCP schema:

| SDK Version | UCP Schema Version |
| ----------------- | ------------------ |
| **`0.4.x`** | **`2026-04-08`** |
| **`0.5.x`** | **`2026-08-25`** |
| `0.4.x` | `2026-04-08` |
| `0.3.x` | `2026-01-23` |
| `0.2.x` / `0.1.x` | `2026-01-11` |

Expand Down Expand Up @@ -71,20 +72,22 @@ from ucp_sdk.models.schemas.shopping.checkout import Checkout
checkout = Checkout.model_validate(checkout_data)

# Access typed fields
print(checkout.status) # "incomplete" | "ready_for_complete" | ...
print(checkout.currency) # ISO 4217 currency code
print(checkout.status) # "incomplete" | "ready_for_complete" | ...
print(checkout.currency) # ISO 4217 currency code
for item in checkout.line_items:
print(f"{item.item.title}: {item.quantity}")
```

### Available model packages

| Package | Description |
| --------------------------------------- | --------------------------------------------------- |
| `ucp_sdk.models.schemas.shopping` | Checkout, cart, catalog, order, payment models |
| `ucp_sdk.models.schemas.shopping.types` | Line items, totals, buyer, fulfillment, signals |
| `ucp_sdk.models.schemas.transports` | REST, MCP, and embedded protocol bindings |
| `ucp_sdk.models.schemas` | Service definitions, capabilities, payment handlers |
| Package | Description |
| --------------------------------------- | ----------------------------------------------------------------- |
| `ucp_sdk.models.schemas.common` | Location search/lookup, identity linking, loyalty, payment terms |
| `ucp_sdk.models.schemas.common.types` | Shared primitives (amounts, prices, totals, postal address, etc.) |
| `ucp_sdk.models.schemas.shopping` | Checkout, cart, catalog, order, buyer consent, permalink models |
| `ucp_sdk.models.schemas.shopping.types` | Line items, adjustments, fulfillments, destinations, attribution |
| `ucp_sdk.models.schemas.transports` | REST, MCP, and embedded protocol bindings |
| `ucp_sdk.models.schemas` | Service definitions, capabilities, profile, payment handlers |

### Validation

Expand Down Expand Up @@ -133,7 +136,7 @@ uv sync
```

Where `<version>` is the version of the UCP specification to use (for example,
"2026-04-08").
"2026-08-25").

If no version is specified, the `main` branch of the
[UCP repo](https://github.com/Universal-Commerce-Protocol/ucp) will be used.
Expand Down
20 changes: 12 additions & 8 deletions generate_models.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,6 @@ if ! command -v git &> /dev/null; then
exit 1
fi

# Check if uv is installed before cloning or modifying generated files
if ! command -v uv &> /dev/null; then
echo "Error: uv not found."
echo "Please install uv: curl -LsSf https://astral.sh/uv/install.sh | sh"
exit 1
fi

# UCP Version to use (if provided, use release/$1 branch; otherwise, use main)
if [ -z "$1" ]; then
BRANCH="main"
Expand All @@ -46,6 +39,7 @@ fi
# Ensure ucp directory is clean before cloning
rm -rf ucp
git clone -b "$BRANCH" --depth 1 https://github.com/Universal-Commerce-Protocol/ucp ucp
rm -rf ucp/.git

# Output directory
OUTPUT_DIR="src/ucp_sdk/models/schemas"
Expand Down Expand Up @@ -101,9 +95,19 @@ uv run \
echo "Post-processing generated models (constraints the generator ignores)..."
uv run python postprocess_models.py || exit 1

# Normalize file endings (as pre-commit's end-of-file-fixer does)
python3 - <<'PY'
from pathlib import Path

for path in Path("src/ucp_sdk/models/schemas").rglob("*.py"):
text = path.read_text(encoding="utf-8")
fixed = text.rstrip("\n") + "\n" if text.strip() else ""
if fixed != text:
path.write_text(fixed, encoding="utf-8")
PY

echo "Formatting generated models..."
uv run ruff format
uv run ruff check --fix "$OUTPUT_DIR"


echo "Done. Models generated in $OUTPUT_DIR"
2 changes: 1 addition & 1 deletion postprocess_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ def find_root_min_properties(schema_dir):
"cannot map to a class\n"
)
continue
found[title] = minimum
found[_alias_name(title)] = minimum
return found


Expand Down
24 changes: 16 additions & 8 deletions preprocess_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,15 @@ def _process_all_of_item(item, node, root, state):

# Extract polymorphic branches (anyOf, oneOf) to keep the node flat
for poly_key in ["anyOf", "oneOf"]:
if poly_key in item:
if poly_key in item and isinstance(item[poly_key], list):
state["poly_branches"].setdefault(poly_key, []).extend(
item.pop(poly_key)
)

# Merge core property definitions and requirements
if "properties" in item:
if "properties" in item and isinstance(item["properties"], dict):
state["merged_properties"].update(item["properties"])
if "required" in item:
if "required" in item and isinstance(item["required"], list):
for req in item["required"]:
if req not in state["merged_required"]:
state["merged_required"].append(req)
Expand Down Expand Up @@ -196,7 +196,7 @@ def merge_all_of_to_node(node, root):
models if inheritance is flattened at the schema level rather than relying on
complex 'allOf' chains which can lead to redundant intermediate classes.
"""
if "allOf" not in node:
if "allOf" not in node or not isinstance(node["allOf"], list):
return

all_of_sources = node.pop("allOf")
Expand All @@ -221,15 +221,19 @@ def distribute_properties_to_branches(node):
model in Pydantic. Without this, a generated union model might miss required
common fields if it's treated as a pure 'oneOf' alternative.
"""
if "properties" not in node:
if "properties" not in node or not isinstance(node["properties"], dict):
return

base_props = node["properties"]
base_req = node.get("required", [])
base_req = (
node.get("required", [])
if isinstance(node.get("required"), list)
else []
)
base_type = node.get("type")

for poly_key in ["anyOf", "oneOf"]:
if poly_key not in node:
if poly_key not in node or not isinstance(node[poly_key], list):
continue

updated_branches = []
Expand Down Expand Up @@ -263,7 +267,7 @@ def flatten_entity_reference(node, entity_definition):
Replaces $ref to 'ucp.json#/$defs/entity' with actual logic.
This effectively converts 'Entity' inheritance into direct 'BaseModel' fields.
"""
if "allOf" not in node:
if "allOf" not in node or not isinstance(node["allOf"], list):
return

filtered_all_of = []
Expand Down Expand Up @@ -291,6 +295,10 @@ def preprocess_full_schema(schema, entity_def=None):
Main entry point for normalizing a single schema file.
Uses bottom-up iteration to ensure nested structures are flat before parents process them.
"""
# Remove $id so datamodel-code-generator resolves relative $refs strictly
# via the local filesystem rather than attempting remote HTTP fetching.
schema.pop("$id", None)

# 1. Discovery: find all dictionaries in the tree
nodes = [n for n in iter_nodes(schema) if isinstance(n, dict)]

Expand Down
17 changes: 7 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
[project]
name = "ucp-sdk"
version = "0.4.6"
version = "0.5.0"
description = "UCP Python SDK"
readme = "README.md"
license = {file = "LICENSE"}
authors = [
{ name = "Enric Cusell", email = "cusell@google.com" },
{ name = "Federico D'Amato", email = "damaz@google.com" }
]
classifiers = [
Expand All @@ -26,14 +27,7 @@ dependencies = [

[dependency-groups]
dev = [
# Exact pins: the model-drift CI job regenerates the models against the
# pinned UCP spec and asserts byte-identical output, so the codegen
# toolchain must resolve reproducibly. (The floating ">=0.50.0" bound
# broke on 2026-08-03: datamodel-code-generator 0.72.0 mis-resolves the
# spec's remote $refs and emits incomplete models.) Bump these pins and
# regenerate the models in the same PR.
"datamodel-code-generator[http]==0.71.0",
"ruff==0.16.1",
"datamodel-code-generator[http,ruff]>=0.50.0",
]

[build-system]
Expand Down Expand Up @@ -75,7 +69,10 @@ skip-magic-trailing-comma = false
line-ending = "auto"

[tool.ruff.lint.per-file-ignores]
"src/ucp_sdk/models/schemas/**/*.py" = ["E501", "D", "N801"]
"src/ucp_sdk/models/schemas/**/*.py" = ["E501", "D", "N801", "N815"]
"preprocess_schemas.py" = ["E501", "D"]
"postprocess_models.py" = ["E501", "D"]
"tests/**/*.py" = ["E501", "D"]

[tool.ruff.lint]
select = ["E", "F", "W", "B", "C4", "SIM", "N", "UP", "D", "PTH", "T20"]
Expand Down
17 changes: 17 additions & 0 deletions src/ucp_sdk/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright 2026 UCP Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""UCP Python SDK."""

__version__ = "0.5.0"
15 changes: 15 additions & 0 deletions src/ucp_sdk/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright 2026 UCP Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""UCP models."""
Loading
Loading