Skip to content

Commit 9a1a921

Browse files
authored
Update SDK to 2026-01-23 (#5)
* update SDK to 2026-01-23 * add init at the base level * update README * add request specific classes * fix schema generation * fix schema generation * Update pyproject.toml * update pre-processing to correctly handle self-reference * fix * sync * address comments Change-Id: I7400c7bd870e3505d5b8c9a9e179d305c65bb3dc * add win32 specific check for paths Change-Id: I4583f94619b072eb7aff0fc28f040d0b2fd30de3 * remove preprocessing Change-Id: Ib7d74192095677f98a79906f5ed2d69dc16e1520 * update readme Change-Id: I2e70bdaf468ef7663b6c8e7542f2acf822d9df06 * re-run of generate_models.sh Change-Id: Id33ccc029b86af2486c82dc85d8bb12b3c55c680 * move ruff config Change-Id: Ia518ec52de6906da7262f7a76704a676a6318c40 * delete temp files Change-Id: I113bf00366152f552140a0f6410f738813c44007 * add LICENSE reference Change-Id: I8413a4ad2e3886df6684ff7f654faa2ed2a95043 * add preprocessing to handle request type class generation Change-Id: I63be54dcfc253861c05a9143483fd73a98f2c232 * change naming convention Change-Id: If59a9d92e40d71bc9d69d171b7020310ee9e26a1 * update preprocessing to include more request types Change-Id: I79bccca52f3a1d9d721c6a6f10d128f544411840 * delete ucp Change-Id: I0ac8995f2d15caa97ce57e9a1be574f012d5b9f2
1 parent 8bc00d8 commit 9a1a921

114 files changed

Lines changed: 2075 additions & 1359 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,7 @@ For now, you can install the SDK using the following commands:
3838
mkdir sdk
3939

4040
# Clone the repository
41-
git clone https://github.com/Universal-Commerce-Protocol/python-sdk.git sdk/python
42-
43-
# Navigate to the directory
44-
cd sdk/python
41+
git clone https://github.com/Universal-Commerce-Protocol/python-sdk.git
4542

4643
# Install dependencies
4744
uv sync
@@ -62,9 +59,13 @@ To regenerate the models:
6259

6360
```bash
6461
uv sync
65-
./generate_models.sh
62+
./generate_models.sh <version>
6663
```
6764

65+
Where `<version>` is the version of the UCP specification to use (for example, "2026-01-23").
66+
67+
If no version is specified, the `main` branch of the [UCP repo](https://github.com/Universal-Commerce-Protocol/ucp) will be used.
68+
6869
The generated code is automatically formatted using `ruff`.
6970

7071
## Contributing

generate_models.sh

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,38 @@
44
# Ensure we are in the script's directory
55
cd "$(dirname "$0")" || exit
66

7+
# Add ~/.local/bin to PATH for uv
8+
export PATH="$HOME/.local/bin:$PATH"
9+
10+
# Check if git is installed
11+
if ! command -v git &> /dev/null; then
12+
echo "Error: git not found. Please install git."
13+
exit 1
14+
fi
15+
16+
# UCP Version to use (if provided, use release/$1 branch; otherwise, use main)
17+
if [ -z "$1" ]; then
18+
BRANCH="main"
19+
echo "No version specified, cloning main branch..."
20+
else
21+
BRANCH="release/$1"
22+
echo "Cloning version $1 (branch: $BRANCH)..."
23+
fi
24+
25+
# Ensure ucp directory is clean before cloning
26+
rm -rf ucp
27+
git clone -b "$BRANCH" --depth 1 https://github.com/Universal-Commerce-Protocol/ucp ucp
28+
729
# Output directory
830
OUTPUT_DIR="src/ucp_sdk/models"
931

1032
# Schema directory (relative to this script)
11-
SCHEMA_DIR="../../spec/"
33+
SCHEMA_DIR="ucp/source"
34+
35+
echo "Preprocessing schemas..."
36+
uv run python preprocess_schemas.py
1237

13-
echo "Generating Pydantic models from $SCHEMA_DIR..."
38+
echo "Generating Pydantic models from preprocessed schemas..."
1439

1540
# Check if uv is installed
1641
if ! command -v uv &> /dev/null; then
@@ -23,9 +48,11 @@ fi
2348
rm -r -f "$OUTPUT_DIR"
2449
mkdir -p "$OUTPUT_DIR"
2550

51+
2652
# Run generation using uv
2753
# We use --use-schema-description to use descriptions from JSON schema as docstrings
2854
# We use --field-constraints to include validation constraints (regex, min/max, etc.)
55+
# Note: Formatting is done as a post-processing step.
2956
uv run \
3057
--link-mode=copy \
3158
--extra-index-url https://pypi.org/simple python \
@@ -41,7 +68,11 @@ uv run \
4168
--disable-timestamp \
4269
--use-double-quotes \
4370
--no-use-annotated \
44-
--allow-extra-fields \
45-
--formatters ruff-format ruff-check
71+
--allow-extra-fields
72+
73+
echo "Formatting generated models..."
74+
uv run ruff format
75+
uv run ruff check --fix "$OUTPUT_DIR" 2>&1 | grep -E "^(All checks passed|Fixed|Found)" || echo "Formatting complete"
76+
4677

4778
echo "Done. Models generated in $OUTPUT_DIR"

preprocess_schemas.py

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
# Copyright 2026 UCP Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import json
16+
import copy
17+
from pathlib import Path
18+
import sys
19+
20+
21+
def get_explicit_ops(schema):
22+
"""Finds ops explicitly mentioned in ucp_request fields."""
23+
ops = set()
24+
properties = schema.get("properties", {})
25+
for prop_data in properties.values():
26+
if not isinstance(prop_data, dict):
27+
continue
28+
ucp_req = prop_data.get("ucp_request")
29+
if isinstance(ucp_req, str):
30+
# Strings like "omit" or "required" only imply standard ops.
31+
# "complete" request should only be generated when it's explicitly defined in a dict.
32+
ops.update(["create", "update"])
33+
elif isinstance(ucp_req, dict):
34+
for op in ucp_req:
35+
ops.add(op)
36+
return ops
37+
38+
39+
def get_props_with_refs(schema, schema_file_path):
40+
"""Finds all external schema references associated with their properties."""
41+
results = [] # list of (prop_name, abs_ref_path)
42+
43+
def find_refs(obj, prop_name):
44+
if isinstance(obj, dict):
45+
if "$ref" in obj:
46+
ref = obj["$ref"]
47+
if "#" not in ref:
48+
ref_path = (schema_file_path.parent / ref).resolve()
49+
results.append((prop_name, str(ref_path)))
50+
for v in obj.values():
51+
find_refs(v, prop_name)
52+
elif isinstance(obj, list):
53+
for item in obj:
54+
find_refs(item, prop_name)
55+
56+
properties = schema.get("properties", {})
57+
for prop_name, prop_data in properties.items():
58+
find_refs(prop_data, prop_name)
59+
return results
60+
61+
62+
def get_variant_filename(base_path, op):
63+
p = Path(base_path)
64+
return p.parent / f"{p.stem}_{op}_request.json"
65+
66+
67+
def generate_variants(schema_file, schema, ops, all_variant_needs):
68+
schema_file_path = Path(schema_file)
69+
for op in ops:
70+
variant_schema = copy.deepcopy(schema)
71+
72+
# Update title and id
73+
base_title = schema.get("title", schema_file_path.stem)
74+
variant_schema["title"] = f"{base_title} {op.capitalize()} Request"
75+
76+
# Update $id if present
77+
if "$id" in variant_schema:
78+
old_id = variant_schema["$id"]
79+
if "/" in old_id:
80+
old_id_parts = old_id.split("/")
81+
old_id_filename = old_id_parts[-1]
82+
if "." in old_id_filename:
83+
stem = old_id_filename.split(".")[0]
84+
ext = old_id_filename.split(".")[-1]
85+
new_id_filename = f"{stem}_{op}_request.{ext}"
86+
variant_schema["$id"] = "/".join(
87+
old_id_parts[:-1] + [new_id_filename]
88+
)
89+
90+
new_properties = {}
91+
new_required = []
92+
93+
for prop_name, prop_data in schema.get("properties", {}).items():
94+
if not isinstance(prop_data, dict):
95+
new_properties[prop_name] = prop_data
96+
continue
97+
98+
ucp_req = prop_data.get("ucp_request")
99+
100+
include = True
101+
is_required = False
102+
103+
if ucp_req is not None:
104+
if isinstance(ucp_req, str):
105+
if ucp_req == "omit":
106+
include = False
107+
elif ucp_req == "required":
108+
is_required = True
109+
elif isinstance(ucp_req, dict):
110+
op_val = ucp_req.get(op)
111+
if op_val == "omit" or op_val is None:
112+
include = False
113+
elif op_val == "required":
114+
is_required = True
115+
else:
116+
# No ucp_request. Include if it was required in base?
117+
if prop_name in schema.get("required", []):
118+
is_required = True
119+
120+
if include:
121+
prop_copy = copy.deepcopy(prop_data)
122+
if "ucp_request" in prop_copy:
123+
del prop_copy["ucp_request"]
124+
125+
# Recursive reference check (deep)
126+
def update_refs(obj):
127+
if isinstance(obj, dict):
128+
if "$ref" in obj:
129+
ref = obj["$ref"]
130+
if "#" not in ref:
131+
ref_path = Path(ref)
132+
target_base_abs = (schema_file_path.parent / ref_path).resolve()
133+
if (
134+
str(target_base_abs) in all_variant_needs
135+
and op in all_variant_needs[str(target_base_abs)]
136+
):
137+
variant_ref_filename = f"{ref_path.stem}_{op}_request.json"
138+
obj["$ref"] = str(ref_path.parent / variant_ref_filename)
139+
for k, v in obj.items():
140+
update_refs(v)
141+
elif isinstance(obj, list):
142+
for item in obj:
143+
update_refs(item)
144+
145+
update_refs(prop_copy)
146+
147+
new_properties[prop_name] = prop_copy
148+
if is_required:
149+
new_required.append(prop_name)
150+
151+
# Always generate the variant schema to avoid breaking refs in parents
152+
variant_schema["properties"] = new_properties
153+
variant_schema["required"] = new_required
154+
155+
variant_path = get_variant_filename(schema_file_path, op)
156+
with open(variant_path, "w") as f:
157+
json.dump(variant_schema, f, indent=2)
158+
print(f"Generated {variant_path}")
159+
160+
161+
def main():
162+
schema_dir = "ucp/source"
163+
if len(sys.argv) > 1:
164+
schema_dir = sys.argv[1]
165+
166+
schema_dir_path = Path(schema_dir)
167+
if not schema_dir_path.exists():
168+
print(f"Directory {schema_dir} does not exist.")
169+
return
170+
171+
all_files = list(schema_dir_path.rglob("*.json"))
172+
173+
schemas_cache = {}
174+
schema_props_refs = {}
175+
all_variant_needs = {}
176+
177+
# 1. First pass: load all schemas and find properties with refs
178+
for f in all_files:
179+
if "_request.json" in f.name:
180+
continue
181+
try:
182+
with open(f, "r") as open_f:
183+
schema = json.load(open_f)
184+
if (
185+
not isinstance(schema, dict)
186+
or schema.get("type") != "object"
187+
or "properties" not in schema
188+
):
189+
continue
190+
191+
abs_path = str(f.resolve())
192+
schemas_cache[abs_path] = schema
193+
schema_props_refs[abs_path] = get_props_with_refs(schema, f)
194+
195+
# 2. Get explicit needs defined in the schema itself
196+
explicit_ops = get_explicit_ops(schema)
197+
if explicit_ops:
198+
all_variant_needs[abs_path] = explicit_ops
199+
except Exception as e:
200+
print(f"Error processing {f}: {e}")
201+
202+
# 3. Transitive dependency tracking (Parent -> Child):
203+
# If P needs variant OP, and P includes property S (not omitted for OP),
204+
# then S also needs variant OP to ensure ref matching works correctly.
205+
changed = True
206+
while changed:
207+
changed = False
208+
for abs_path, props_refs in schema_props_refs.items():
209+
if abs_path not in all_variant_needs:
210+
continue
211+
212+
parent_schema = schemas_cache[abs_path]
213+
parent_ops = all_variant_needs[abs_path]
214+
215+
for op in list(parent_ops):
216+
for prop_name, ref_path in props_refs:
217+
if ref_path not in schemas_cache:
218+
continue
219+
220+
# Check if this property is omitted for this op in parent
221+
prop_data = parent_schema["properties"].get(prop_name, {})
222+
ucp_req = prop_data.get("ucp_request")
223+
224+
include = True
225+
if ucp_req is not None:
226+
if isinstance(ucp_req, str):
227+
if ucp_req == "omit":
228+
include = False
229+
elif isinstance(ucp_req, dict):
230+
op_val = ucp_req.get(op)
231+
if op_val == "omit" or op_val is None:
232+
include = False
233+
234+
if include:
235+
# Propagate op from parent to child
236+
child_needs = all_variant_needs.get(ref_path, set())
237+
if op not in child_needs:
238+
all_variant_needs.setdefault(ref_path, set()).add(op)
239+
changed = True
240+
241+
# 4. Final pass: generate variants
242+
for f_abs, ops in all_variant_needs.items():
243+
generate_variants(f_abs, schemas_cache[f_abs], ops, all_variant_needs)
244+
245+
246+
if __name__ == "__main__":
247+
main()

pyproject.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
[project]
22
name = "ucp-sdk"
3-
version = "0.1.0"
3+
version = "2026.01.23"
44
description = "UCP Python SDK"
55
readme = "README.md"
6+
license-files = ["LICENSE"]
67
authors = [
7-
{ name = "Florin Iucha", email = "fiucha@google.com" }
8+
{ name = "Florin Iucha", email = "fiucha@google.com" },
9+
{ name = "Federico D'Amato", email = "damaz@google.com" },
810
]
911
requires-python = ">=3.10"
1012
dependencies = [
@@ -62,4 +64,4 @@ select = ["E", "F", "W", "B", "C4", "SIM", "N", "UP", "D", "PTH", "T20"]
6264
[tool.ruff.lint.isort]
6365
combine-as-imports = true
6466
force-sort-within-sections = true
65-
case-sensitive = true
67+
case-sensitive = true

src/ucp_sdk/__init__.py

Lines changed: 0 additions & 19 deletions
This file was deleted.

src/ucp_sdk/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@
1515
# generated by datamodel-codegen
1616
# pylint: disable=all
1717
# pyformat: disable
18+

0 commit comments

Comments
 (0)