Skip to content

feat: upgrade Python SDK to UCP v2026-04-08#48

Open
segiodongo wants to merge 2 commits into
Universal-Commerce-Protocol:mainfrom
segiodongo:feat/v48-upgrade
Open

feat: upgrade Python SDK to UCP v2026-04-08#48
segiodongo wants to merge 2 commits into
Universal-Commerce-Protocol:mainfrom
segiodongo:feat/v48-upgrade

Conversation

@segiodongo

Copy link
Copy Markdown

Regenerates Pydantic models against UCP April 8th (v48) schemas, updates package metadata to 0.4.0, and configures per-file-ignores for generated docstrings.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the UCP Python SDK schemas to support new shopping cart, catalog lookup, catalog search, and order lifecycle capabilities, while transitioning monetary fields to use typed Amount and SignedAmount models. The code review feedback suggests enhancing several of these generated Pydantic models with runtime validators to enforce documented constraints, such as ensuring at least one format is provided in Description, exactly one subtotal and total entry exist in Totals, valid price ranges in PriceFilter, proper rating bounds in Rating, and cursor presence when pagination has a next page.

Comment on lines +21 to +43
from pydantic import BaseModel, ConfigDict


class Description(BaseModel):
"""
Description content in one or more formats. At least one format must be provided.
"""

model_config = ConfigDict(
extra="allow",
)
plain: str | None = None
"""
Plain text content.
"""
html: str | None = None
"""
HTML-formatted content. Security: Platforms MUST sanitize before rendering—strip scripts, event handlers, and untrusted elements. Treat all rich text as untrusted input.
"""
markdown: str | None = None
"""
Markdown-formatted content.
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The docstring specifies that 'At least one format must be provided' for the Description model. However, all fields (plain, html, markdown) are currently optional with no validation enforcing this constraint. Adding a model validator ensures compliance with the schema definition.

from pydantic import BaseModel, ConfigDict, model_validator


class Description(BaseModel):
    """
    Description content in one or more formats. At least one format must be provided.
    """

    model_config = ConfigDict(
        extra="allow",
    )
    plain: str | None = None
    """
    Plain text content.
    """
    html: str | None = None
    """
    HTML-formatted content. Security: Platforms MUST sanitize before rendering—strip scripts, event handlers, and untrusted elements. Treat all rich text as untrusted input.
    """
    markdown: str | None = None
    """
    Markdown-formatted content.
    """

    @model_validator(mode="after")
    def validate_at_least_one_format(self) -> Description:
        if self.plain is None and self.html is None and self.markdown is None:
            raise ValueError("At least one description format (plain, html, markdown) must be provided.")
        return self


from __future__ import annotations

from pydantic import BaseModel, ConfigDict, Field, RootModel

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Import model_validator to support validation of totals entries.

Suggested change
from pydantic import BaseModel, ConfigDict, Field, RootModel
from pydantic import BaseModel, ConfigDict, Field, RootModel, model_validator

Comment on lines +52 to +63
class Totals(RootModel[list[Total]]):
"""
Pricing breakdown provided by the business. MUST contain exactly one subtotal and one total entry. Detail types (tax, fee, discount, fulfillment) may appear multiple times for itemization. Platforms MUST render all entries in order using display_text and amount.
"""

model_config = ConfigDict(
frozen=True,
)
root: list[Total] = Field(..., title="Totals")
"""
Pricing breakdown provided by the business. MUST contain exactly one subtotal and one total entry. Detail types (tax, fee, discount, fulfillment) may appear multiple times for itemization. Platforms MUST render all entries in order using display_text and amount.
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The docstring specifies that the Totals model 'MUST contain exactly one subtotal and one total entry.' Adding a model validator enforces this structural constraint at runtime.

Suggested change
class Totals(RootModel[list[Total]]):
"""
Pricing breakdown provided by the business. MUST contain exactly one subtotal and one total entry. Detail types (tax, fee, discount, fulfillment) may appear multiple times for itemization. Platforms MUST render all entries in order using display_text and amount.
"""
model_config = ConfigDict(
frozen=True,
)
root: list[Total] = Field(..., title="Totals")
"""
Pricing breakdown provided by the business. MUST contain exactly one subtotal and one total entry. Detail types (tax, fee, discount, fulfillment) may appear multiple times for itemization. Platforms MUST render all entries in order using display_text and amount.
"""
class Totals(RootModel[list[Total]]):
"""
Pricing breakdown provided by the business. MUST contain exactly one subtotal and one total entry. Detail types (tax, fee, discount, fulfillment) may appear multiple times for itemization. Platforms MUST render all entries in order using display_text and amount.
"""
model_config = ConfigDict(
frozen=True,
)
root: list[Total] = Field(..., title="Totals")
"""
Pricing breakdown provided by the business. MUST contain exactly one subtotal and one total entry. Detail types (tax, fee, discount, fulfillment) may appear multiple times for itemization. Platforms MUST render all entries in order using display_text and amount.
"""
@model_validator(mode="after")
def validate_totals_entries(self) -> Totals:
subtotal_count = sum(1 for t in self.root if t.type == "subtotal")
total_count = sum(1 for t in self.root if t.type == "total")
if subtotal_count != 1 or total_count != 1:
raise ValueError("Totals must contain exactly one subtotal and one total entry.")
return self

Comment on lines +20 to +40

from pydantic import BaseModel, ConfigDict

from . import amount


class PriceFilter(BaseModel):
"""
Price range filter denominated in context.currency. When context.currency matches the presentment currency, businesses apply the filter directly. When it differs, businesses SHOULD convert filter values to the presentment currency before applying; if conversion is not supported, businesses MAY ignore the filter and SHOULD indicate this via a message. When context.currency is absent, filter denomination is ambiguous and businesses MAY ignore it.
"""

model_config = ConfigDict(
extra="allow",
)
min: amount.Amount | None = None
"""
Minimum price in ISO 4217 minor units.
"""
max: amount.Amount | None = None
"""
Maximum price in ISO 4217 minor units.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Add a model validator to ensure that the minimum price (min) is not greater than the maximum price (max) when both are provided.

from pydantic import BaseModel, ConfigDict, model_validator

from . import amount


class PriceFilter(BaseModel):
    """
    Price range filter denominated in context.currency. When context.currency matches the presentment currency, businesses apply the filter directly. When it differs, businesses SHOULD convert filter values to the presentment currency before applying; if conversion is not supported, businesses MAY ignore the filter and SHOULD indicate this via a message. When context.currency is absent, filter denomination is ambiguous and businesses MAY ignore it.
    """

    model_config = ConfigDict(
        extra="allow",
    )
    min: amount.Amount | None = None
    """
    Minimum price in ISO 4217 minor units.
    """
    max: amount.Amount | None = None
    """
    Maximum price in ISO 4217 minor units.
    """

    @model_validator(mode="after")
    def validate_price_range(self) -> PriceFilter:
        if self.min is not None and self.max is not None:
            if self.min.root > self.max.root:
                raise ValueError("Minimum price (min) cannot be greater than maximum price (max).")
        return self

Comment on lines +20 to +47

from pydantic import BaseModel, ConfigDict, Field


class Rating(BaseModel):
"""
Product rating aggregate.
"""

model_config = ConfigDict(
extra="allow",
)
value: float = Field(..., ge=0.0)
"""
Average rating value.
"""
scale_min: float | None = Field(1, ge=0.0)
"""
Minimum value on the rating scale (e.g., 1 for 1-5 stars).
"""
scale_max: float = Field(..., ge=1.0)
"""
Maximum value on the rating scale (e.g., 5 for 5-star).
"""
count: int | None = Field(None, ge=0)
"""
Number of reviews contributing to the rating.
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Add a model validator to ensure that scale_min is strictly less than scale_max, and that the rating value lies within the [scale_min, scale_max] range.

from pydantic import BaseModel, ConfigDict, Field, model_validator


class Rating(BaseModel):
    """
    Product rating aggregate.
    """


    model_config = ConfigDict(
        extra="allow",
    )
    value: float = Field(..., ge=0.0)
    """
    Average rating value.
    """
    scale_min: float | None = Field(1, ge=0.0)
    """
    Minimum value on the rating scale (e.g., 1 for 1-5 stars).
    """
    scale_max: float = Field(..., ge=1.0)
    """
    Maximum value on the rating scale (e.g., 5 for 5-star).
    """
    count: int | None = Field(None, ge=0)
    """
    Number of reviews contributing to the rating.
    """

    @model_validator(mode="after")
    def validate_rating_bounds(self) -> Rating:
        min_val = self.scale_min if self.scale_min is not None else 1.0
        if min_val >= self.scale_max:
            raise ValueError("scale_min must be less than scale_max.")
        if not (min_val <= self.value <= self.scale_max):
            raise ValueError(f"Rating value {self.value} must be between {min_val} and {self.scale_max}.")
        return self

# pyformat: disable

from __future__ import annotations

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Import model_validator to support cursor presence validation.

Suggested change
from pydantic import BaseModel, ConfigDict, Field, model_validator

Comment on lines +52 to +71
class Response(BaseModel):
"""
Pagination information in responses.
"""

model_config = ConfigDict(
extra="allow",
)
cursor: str | None = None
"""
Cursor to fetch the next page of results. MUST be present when has_next_page is true.
"""
has_next_page: bool
"""
Whether more results are available.
"""
total_count: int | None = Field(None, ge=0)
"""
Total number of matching items, if available.
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The docstring for cursor specifies that it 'MUST be present when has_next_page is true.' Adding a model validator enforces this requirement at runtime.

class Response(BaseModel):
    """
    Pagination information in responses.
    """

    model_config = ConfigDict(
        extra="allow",
    )
    cursor: str | None = None
    """
    Cursor to fetch the next page of results. MUST be present when has_next_page is true.
    """
    has_next_page: bool
    """
    Whether more results are available.
    """
    total_count: int | None = Field(None, ge=0)
    """
    Total number of matching items, if available.
    """

    @model_validator(mode="after")
    def validate_cursor_presence(self) -> Response:
        if self.has_next_page and self.cursor is None:
            raise ValueError("cursor must be present when has_next_page is True.")
        return self

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant