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
2 changes: 1 addition & 1 deletion rest/python/client/flower_shop/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ packages = ["."]

[tool.uv.sources]
# The relative path is stored here
ucp-sdk = { path = "../../../../../sdk/python/", editable = true }
ucp-sdk = { path = "../../../../../python-sdk/", editable = true }

[tool.ruff]
line-length = 80
Expand Down
77 changes: 24 additions & 53 deletions rest/python/client/flower_shop/simple_happy_path_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,11 @@
from ucp_sdk.models.schemas.shopping import checkout_create_req
from ucp_sdk.models.schemas.shopping import checkout_update_req
from ucp_sdk.models.schemas.shopping import payment_create_req
from ucp_sdk.models.schemas.shopping.payment_data import PaymentData
from ucp_sdk.models.schemas.shopping.types import buyer
from ucp_sdk.models.schemas.shopping.types import item_create_req
from ucp_sdk.models.schemas.shopping.types import item_update_req
from ucp_sdk.models.schemas.shopping.types import line_item_create_req
from ucp_sdk.models.schemas.shopping.types import line_item_update_req
from ucp_sdk.models.schemas.shopping.types.card_payment_instrument import (
CardPaymentInstrument,
)
from ucp_sdk.models.schemas.shopping.types.payment_instrument import (
PaymentInstrument,
)
from ucp_sdk.models.schemas.shopping.types.postal_address import PostalAddress
from ucp_sdk.models.schemas.shopping.types.token_credential_resp import (
TokenCredentialResponse,
)


def get_headers() -> dict[str, str]:
Expand Down Expand Up @@ -805,48 +794,30 @@ def main() -> None:

# Matches the structure expected by the server's updated complete_checkout

billing_address = PostalAddress(
street_address="123 Main St",
address_locality="Anytown",
address_region="CA",
address_country="US",
postal_code="12345",
)

credential = TokenCredentialResponse(type="token", token="success_token")

instr = CardPaymentInstrument(
id="instr_my_card",
handler_id=target_handler,
handler_name=target_handler,
type="card",
brand="Visa",
last_digits="4242",
credential=credential,
billing_address=billing_address,
)

# Wrapped in RootModel

wrapped_instr = PaymentInstrument(root=instr)

# Use PaymentData to wrap the payload

final_req = PaymentData(payment_data=wrapped_instr)

# Add risk_signals as extra fields (since it's not explicitly in the model)

# Using model_extra or just passing to constructor if allow_extra is true

# PaymentData allows extra.

final_payload = final_req.model_dump(
mode="json", by_alias=True, exclude_none=True
)

final_payload["risk_signals"] = {
"ip": "127.0.0.1",
"browser": "python-httpx",
final_payload = {
"payment_data": {
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.

this does not seem a v23 payload. i'd expect "payment", not "payment_data"

"id": "instr_my_card",
"handler_id": target_handler,
"handler_name": target_handler,
"type": "card",
"brand": "Visa",
"last_digits": "4242",
"credential": {
"type": "token",
"token": "success_token",
},
"billing_address": {
"street_address": "123 Main St",
"address_locality": "Anytown",
"address_region": "CA",
"address_country": "US",
"postal_code": "12345",
},
},
"risk_signals": {
"ip": "127.0.0.1",
"browser": "python-httpx",
},
}

headers = get_headers()
Expand Down
5 changes: 2 additions & 3 deletions rest/python/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,8 @@ NOTE: Temporarily the Samples repository expects the SDK at a known relative
filesystem location, as such, the target paths in these example are significant.

```shell
mkdir sdk
git clone https://github.com/Universal-Commerce-Protocol/python-sdk.git sdk/python
pushd sdk/python
git clone https://github.com/Universal-Commerce-Protocol/python-sdk.git
pushd python-sdk
uv sync
popd
git clone https://github.com/Universal-Commerce-Protocol/samples.git
Expand Down
24 changes: 12 additions & 12 deletions rest/python/server/generated_routes/ucp_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,26 @@

from typing import Annotated
from fastapi import APIRouter, Body, Header
import ucp_sdk.models.schemas.shopping.checkout_create_req
import ucp_sdk.models.schemas.shopping.checkout_resp
import ucp_sdk.models.schemas.shopping.checkout_update_req
import ucp_sdk.models.schemas.shopping.checkout_create_request
import ucp_sdk.models.schemas.shopping.checkout
import ucp_sdk.models.schemas.shopping.checkout_update_request
import ucp_sdk.models.schemas.shopping.order
import ucp_sdk.models.schemas.shopping.payment_create_req
import ucp_sdk.models.schemas.shopping.payment_resp
import ucp_sdk.models.schemas.shopping.payment_create_request
import ucp_sdk.models.schemas.shopping.payment

router = APIRouter()


@router.post(
"/checkout-sessions",
response_model=ucp_sdk.models.schemas.shopping.checkout_resp.CheckoutResponse,
response_model=ucp_sdk.models.schemas.shopping.checkout.Checkout,
status_code=201,
operation_id="create_checkout",
summary="Create Checkout",
)
async def create_checkout(
body: Annotated[
ucp_sdk.models.schemas.shopping.checkout_create_req.CheckoutCreateRequest,
ucp_sdk.models.schemas.shopping.checkout_create_request.CheckoutCreateRequest,
Body(...),
],
authorization: str = Header(None, alias="Authorization"),
Expand All @@ -42,7 +42,7 @@ async def create_checkout(

@router.get(
"/checkout-sessions/{id}",
response_model=ucp_sdk.models.schemas.shopping.checkout_resp.CheckoutResponse,
response_model=ucp_sdk.models.schemas.shopping.checkout.Checkout,
status_code=200,
operation_id="get_checkout",
summary="Get Checkout",
Expand All @@ -67,15 +67,15 @@ async def get_checkout(

@router.put(
"/checkout-sessions/{id}",
response_model=ucp_sdk.models.schemas.shopping.checkout_resp.CheckoutResponse,
response_model=ucp_sdk.models.schemas.shopping.checkout.Checkout,
status_code=200,
operation_id="update_checkout",
summary="Update Checkout",
)
async def update_checkout(
id: str,
body: Annotated[
ucp_sdk.models.schemas.shopping.checkout_update_req.CheckoutUpdateRequest,
ucp_sdk.models.schemas.shopping.checkout_update_request.CheckoutUpdateRequest,
Body(...),
],
authorization: str = Header(None, alias="Authorization"),
Expand All @@ -96,7 +96,7 @@ async def update_checkout(

@router.post(
"/checkout-sessions/{id}/complete",
response_model=ucp_sdk.models.schemas.shopping.checkout_resp.CheckoutResponse,
response_model=ucp_sdk.models.schemas.shopping.checkout.Checkout,
status_code=200,
operation_id="complete_checkout",
summary="Complete Checkout",
Expand All @@ -122,7 +122,7 @@ async def complete_checkout(

@router.post(
"/checkout-sessions/{id}/cancel",
response_model=ucp_sdk.models.schemas.shopping.checkout_resp.CheckoutResponse,
response_model=ucp_sdk.models.schemas.shopping.checkout.Checkout,
status_code=200,
operation_id="cancel_checkout",
summary="Cancel Checkout",
Expand Down
100 changes: 51 additions & 49 deletions rest/python/server/integration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,34 +31,47 @@
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.sql import delete
from ucp_sdk.models.schemas.shopping import checkout_create_req
from ucp_sdk.models.schemas.shopping import payment_create_req
from ucp_sdk.models.schemas.shopping.ap2_mandate import (
CheckoutResponseWithAp2 as Ap2Checkout,
from ucp_sdk.models.schemas.shopping import (
checkout_create_request as checkout_create_req,
)
from ucp_sdk.models.schemas.shopping.buyer_consent_resp import (
from ucp_sdk.models.schemas.shopping import (
payment_create_request as payment_create_req,
)
from ucp_sdk.models.schemas.shopping.ap2_mandate import Checkout as Ap2Checkout
from ucp_sdk.models.schemas.shopping.buyer_consent import (
Checkout as BuyerConsentCheckoutResp,
)
from ucp_sdk.models.schemas.shopping.discount_resp import (
from ucp_sdk.models.schemas.shopping.discount import (
Checkout as DiscountCheckoutResp,
)
from ucp_sdk.models.schemas.shopping.fulfillment_create_req import Fulfillment
from ucp_sdk.models.schemas.shopping.fulfillment_resp import (
from ucp_sdk.models.schemas.shopping.fulfillment import (
Checkout as Fulfillment,
)
from ucp_sdk.models.schemas.shopping.fulfillment import (
Checkout as FulfillmentCheckout,
)
from ucp_sdk.models.schemas.shopping.order import PlatformConfig
from ucp_sdk.models.schemas.shopping.payment_data import PaymentData
from ucp_sdk.models.schemas.shopping.order import PlatformSchema
from ucp_sdk.models.schemas.shopping.types import card_payment_instrument
from ucp_sdk.models.schemas.shopping.types import fulfillment_destination_req
from ucp_sdk.models.schemas.shopping.types import fulfillment_group_create_req
from ucp_sdk.models.schemas.shopping.types import fulfillment_method_create_req
from ucp_sdk.models.schemas.shopping.types import fulfillment_req
from ucp_sdk.models.schemas.shopping.types import item_create_req
from ucp_sdk.models.schemas.shopping.types import line_item_create_req
from ucp_sdk.models.schemas.shopping.types import payment_handler_create_req
from ucp_sdk.models.schemas.shopping.types import (
fulfillment_group_create_request as fulfillment_group_create_req,
)
from ucp_sdk.models.schemas.shopping.types import (
fulfillment_method_create_request as fulfillment_method_create_req,
)
from ucp_sdk.models.schemas.shopping.types import fulfillment as fulfillment_req
from ucp_sdk.models.schemas.shopping.types import (
item_create_request as item_create_req,
)
from ucp_sdk.models.schemas.shopping.types import (
line_item_create_request as line_item_create_req,
)
from ucp_sdk.models.schemas.shopping.types import payment_instrument
from ucp_sdk.models.schemas.shopping.types import shipping_destination_req
from ucp_sdk.models.schemas.shopping.types import token_credential_resp
from ucp_sdk.models.schemas.shopping.types import (
shipping_destination as shipping_destination_req,
)
from ucp_sdk.models.schemas.shopping.types import (
token_credential as token_credential_resp,
)

FLAGS = flags.FLAGS

Expand All @@ -71,7 +84,7 @@ class TestCheckout(
):
"""Checkout model supporting Fulfillment, Discount, and AP2 extensions."""

platform: PlatformConfig | None = None
platform: PlatformSchema | None = None


class IntegrationTest(absltest.TestCase):
Expand Down Expand Up @@ -223,37 +236,24 @@ def _create_checkout_payload(
)
line_items.append(line_item)

handler = payment_handler_create_req.PaymentHandlerCreateRequest(
id="google_pay",
name="google.pay",
version="2026-01-11",
spec="https://example.com/spec",
config_schema="https://example.com/schema",
instrument_schemas=["https://example.com/schema"],
config={},
)

payment = payment_create_req.PaymentCreateRequest(
handlers=[handler], instruments=[]
)
payment = payment_create_req.PaymentCreateRequest(instruments=[])

# Hierarchical Fulfillment Construction
destination = fulfillment_destination_req.FulfillmentDestinationRequest(
root=shipping_destination_req.ShippingDestinationRequest(
id="dest_1", address_country="US"
)
destination = shipping_destination_req.ShippingDestination(
id="dest_1", address_country="US"
)
group = fulfillment_group_create_req.FulfillmentGroupCreateRequest(
selected_option_id="std-ship"
)
method = fulfillment_method_create_req.FulfillmentMethodCreateRequest(
line_item_ids=[i_id for i_id, _, _, _ in items],
type="shipping",
destinations=[destination],
selected_destination_id="dest_1",
groups=[group],
)
fulfillment = Fulfillment(
root=fulfillment_req.FulfillmentRequest(methods=[method])
root=fulfillment_req.Fulfillment(methods=[method])
)

return checkout_create_req.CheckoutCreateRequest(
Expand All @@ -264,9 +264,9 @@ def _create_checkout_payload(
fulfillment=fulfillment,
)

def _create_payment_payload(self) -> PaymentData:
def _create_payment_payload(self) -> dict:
"""Create a payment payload using SDK models."""
credential = token_credential_resp.TokenCredentialResponse(
credential = token_credential_resp.TokenCredential(
type="token", token="success_token"
)
instrument = card_payment_instrument.CardPaymentInstrument(
Expand All @@ -278,10 +278,12 @@ def _create_payment_payload(self) -> PaymentData:
last_digits="1234",
credential=credential,
)
return PaymentData(
payment_data=payment_instrument.PaymentInstrument(root=instrument),
risk_signals={},
)
return {
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.

can you explain the rationale?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The client post (lines 306-310) require the direct json object or a dictionary. They don't serialize pydantic models, so if we the method returns a class here it would need to be converted or unpacked with "model_dump" like it was before. This is cleaner as it avoids the double conversion.

"payment_data": payment_instrument.PaymentInstrument(
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.

should use a native object, not a json

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

See the reply to your other comment one line above. It's for testing the post payload and it would need to be converted anyway.

root=instrument
).model_dump(mode="json", exclude_none=True),
"risk_signals": {},
}

def test_single_item_checkout(self) -> None:
"""Test the full lifecycle of a single item checkout."""
Expand All @@ -305,7 +307,7 @@ def test_single_item_checkout(self) -> None:
response = self.client.post(
"/checkout-sessions/test_checkout_1/complete",
headers=self._get_headers(idempotency_key="2", request_id="2"),
json=payment_payload.model_dump(mode="json", exclude_none=True),
json=payment_payload,
)
self.assertEqual(response.status_code, 200)
checkout = TestCheckout.model_validate(response.json())
Expand Down Expand Up @@ -353,15 +355,15 @@ def test_double_complete_checkout(self) -> None:
response = self.client.post(
"/checkout-sessions/test_checkout_double/complete",
headers=self._get_headers(idempotency_key="2", request_id="2"),
json=payment_payload.model_dump(mode="json", exclude_none=True),
json=payment_payload,
)
self.assertEqual(response.status_code, 200)

# 3. Complete Checkout (Second time) - Should fail
response = self.client.post(
"/checkout-sessions/test_checkout_double/complete",
headers=self._get_headers(idempotency_key="4", request_id="4"),
json=payment_payload.model_dump(mode="json", exclude_none=True),
json=payment_payload,
)
self.assertEqual(response.status_code, 409)
self.assertEqual(
Expand Down Expand Up @@ -389,7 +391,7 @@ def test_multi_item_checkout(self) -> None:
response = self.client.post(
"/checkout-sessions/test_checkout_multi/complete",
headers=self._get_headers(idempotency_key="6", request_id="6"),
json=payment_payload.model_dump(mode="json", exclude_none=True),
json=payment_payload,
)
self.assertEqual(response.status_code, 200)

Expand Down Expand Up @@ -480,7 +482,7 @@ def test_cancel_checkout(self) -> None:
headers=self._get_headers(
idempotency_key="cancel_5", request_id="cancel_5"
),
json=payment_payload.model_dump(mode="json", exclude_none=True),
json=payment_payload,
)
self.assertEqual(response.status_code, 200)

Expand Down
Loading
Loading