Skip to content

feat(restapi): add Pydantic models for top REST responses (C3) — Cata…#153

Merged
Lenajava1 merged 4 commits into
devfrom
fix/restapi-pydantic-types
Apr 28, 2026
Merged

feat(restapi): add Pydantic models for top REST responses (C3) — Cata…#153
Lenajava1 merged 4 commits into
devfrom
fix/restapi-pydantic-types

Conversation

@Lenajava1
Copy link
Copy Markdown
Collaborator

…log vertical slice

Lays the foundation for typed REST responses (C3 in CODE_REVIEW.md), with Catalog as the first end-to-end migration. Establishes the pattern for follow-up entity migrations.

Foundation

  • restapi/types/{init.py, base.py} — RestModel base mirroring gql/types/GqlModel: snake_case fields, to_camel aliasing, populate_by_name=True, extra="ignore" so unknown server fields are dropped silently.
  • restapi/types/{catalog,category,product,user,role,order}.py — conservative hand-written models covering only the fields the test suite actually touches. Field set sourced from grep across tests/restapi/. Future direction (out of scope here): codegen from the platform's OpenAPI schema.

Catalog vertical slice (the demo)

  • restapi/operations/catalog_operations.py — create() and update() now return Catalog (model_validate on the wire response). search() still returns dict (response shape needs a separate SearchResult model — defer). update() accepts the typed model and dumps via model_dump(by_alias=True).
  • tests/restapi/catalog/conftest.py — make_catalog factory now returns Catalog. make_category accepts either catalog: Catalog or catalog_id: str (the latter for cases where you only have an id).
  • tests/restapi/catalog/test_catalog.py — full sweep from catalog["id"] to catalog.id, etc.
  • tests/restapi/catalog/test_category.py — fixed test_category_nested to pass catalog_id= instead of the previous catalog={"id": ...} hack.
  • tests/restapi/catalog/test_product.py — target_catalog["id"]target_catalog.id in the move_to_catalog test.

Verified

  • 30/30 catalog tests pass on vcptcore-demo
  • pytest --collect-only clean
  • pyright errors on catalog_operations.py: 3 → 1 (the remaining one is search() returning dict, deferred)

Deferred to follow-up PRs (one per entity, mechanical pattern repeat)

  • Category, Product, User, Role, Order operation+consumer migrations. Models are already in place; each follow-up just updates the operation to model_validate() and sweeps test consumers to attribute access.
  • The 5 remaining entities will lift the bulk of the 129 reportReturnType pyright errors surfaced by C2 in the previous PR.

Lenajava1 and others added 3 commits April 28, 2026 19:24
…log vertical slice

Lays the foundation for typed REST responses (C3 in CODE_REVIEW.md), with
Catalog as the first end-to-end migration. Establishes the pattern for
follow-up entity migrations.

Foundation
- restapi/types/{__init__.py, base.py} — `RestModel` base mirroring
  gql/types/GqlModel: snake_case fields, to_camel aliasing, populate_by_name=True,
  extra="ignore" so unknown server fields are dropped silently.
- restapi/types/{catalog,category,product,user,role,order}.py — conservative
  hand-written models covering only the fields the test suite actually touches.
  Field set sourced from grep across tests/restapi/. Future direction
  (out of scope here): codegen from the platform's OpenAPI schema.

Catalog vertical slice (the demo)
- restapi/operations/catalog_operations.py — `create()` and `update()` now
  return `Catalog` (model_validate on the wire response). `search()` still
  returns dict (response shape needs a separate SearchResult model — defer).
  `update()` accepts the typed model and dumps via model_dump(by_alias=True).
- tests/restapi/catalog/conftest.py — `make_catalog` factory now returns
  `Catalog`. `make_category` accepts either `catalog: Catalog` or
  `catalog_id: str` (the latter for cases where you only have an id).
- tests/restapi/catalog/test_catalog.py — full sweep from `catalog["id"]`
  to `catalog.id`, etc.
- tests/restapi/catalog/test_category.py — fixed test_category_nested to
  pass `catalog_id=` instead of the previous `catalog={"id": ...}` hack.
- tests/restapi/catalog/test_product.py — `target_catalog["id"]` →
  `target_catalog.id` in the move_to_catalog test.

Verified
- 30/30 catalog tests pass on vcptcore-demo
- pytest --collect-only clean
- pyright errors on catalog_operations.py: 3 → 1 (the remaining one is
  search() returning dict, deferred)

Deferred to follow-up PRs (one per entity, mechanical pattern repeat)
- Category, Product, User, Role, Order operation+consumer migrations.
  Models are already in place; each follow-up just updates the operation
  to model_validate() and sweeps test consumers to attribute access.
- The 5 remaining entities will lift the bulk of the 129 reportReturnType
  pyright errors surfaced by C2 in the previous PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…er, Role, User

Extends the Catalog vertical slice (commit bfffa4f) to all 6 review-recommended
entities. The original PR scoped down to Catalog only; this push expands to
the full plan per user direction ("review all remain test").

Foundation tweak
- restapi/types/base.py: switch RestModel to extra="allow" so server fields
  outside the typed schema round-trip through model_dump() — needed for
  update() flows that re-send the full server response with edits.
  Tightening to extra="forbid" (S21) is a separate follow-up.
- restapi/types/category.py: add `parent_id` field (used by test_category_nested).

Operations migrated (return typed models for the natural read paths)
- category_operations.py: create/get_by_id/update return Category.
- product_operations.py: create/get_by_ids/get_by_id/update/create_or_update_with_body
  return Product. get_clone() left as dict (it returns a partially-built template).
- order_operations.py: create/get_by_id/get_by_number return CustomerOrder.
  update/recalculate kept dict-friendly because the order test flow round-trips
  the response with deep edits (items[0].quantity, inPayments, shipments) where
  forcing model_dump on every step is more friction than value.
- role_operations.py: create/get_by_name return Role. update accepts Role.
- user_operations.py: get_by_name returns User. create/delete return their
  status dicts ({succeeded, errors}) — that's the platform's contract for those
  endpoints, not a User resource. update accepts User.

Test consumer sweeps
- tests/restapi/catalog/{conftest,test_catalog,test_category,test_product,test_assets}.py:
  attribute access throughout. make_category accepts catalog_id directly
  (cleaner than the previous {"id": ...} dict hack at test_category_nested).
  Server-only fields (weight, images, assets) read via reloaded.model_extra.
- tests/restapi/orders/{conftest,test_orders}.py:
  attribute access for read paths; mutation flows use order.model_dump(by_alias=True)
  before edits. test_order_get_not_found now also catches Pydantic ValidationError
  (the platform returns 200 + null body for missing id, which fails model_validate).
- tests/restapi/platform/{conftest,test_user,test_user_lock,test_user_password,
  test_api_key,test_roles}.py: full_user, reloaded, fetched all switch to
  attribute access (.id, .user_name, .email, .user_type for User; .id, .name
  for Role). make_user keeps returning dict because the create endpoint
  returns a status dict (not a user); credentials remain dict-injected.

Verified
- pytest --collect-only on tests/restapi/: 295 tests collected cleanly
- Live run on vcptcore-demo: 286 passed, 5 failed
  - The 5 failures are the pre-existing personalization async-index issue
    (tracked in project memory; same as previous baseline)
  - The 2 transient order failures from the previous baseline now pass
- No new regressions

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Continues the typed-response migration to the 9 remaining factory-backed
entities, completing the bulk of the C3 work. Pyright reportReturnType
errors should drop substantially after this lands.

New models (restapi/types/)
- contact.py — Contact (id, first_name, last_name, name, member_type)
- organization.py — Organization (id, name, member_type)
- employee.py — Employee (same shape as Contact)
- member.py — Member (id, name, member_type) — generic /api/members
- vendor.py — Vendor (id, name, member_type)
- pricelist.py — Pricelist + PricelistAssignment
- promotion.py — Promotion (id, name, is_active)
- store.py — Store (id, name, catalog, store_state)
- __init__.py — re-exports updated

Operations migrated (read paths return typed models, update returns None)
- contact_operations.py, organization_operations.py, employee_operations.py,
  member_operations.py, vendor_operations.py: create/get_by_id/get_by_ids
  return typed models; update returns None (PUT returns 204).
  bulk variants typed where the server returns the entity list.
- pricelist_operations.py, pricelist_assignment_operations.py: same pattern.
  search() and get_new() kept dict (search shape varies, new is a template).
- promotion_operations.py: same. get_new() kept dict (template).
- store_operations.py: same; get_all() returns list[Store]; get_accessible
  kept list[dict] (response shape unclear).

Factory fixtures
- contacts/conftest.py — make_contact, make_organization, make_employee,
  make_member return typed models.
- pricing/conftest.py — make_pricelist, make_assignment return typed models.
  make_assignment now takes pricelist: Pricelist (was dict).
- marketing/conftest.py — make_promotion returns Promotion.
- store/conftest.py — make_store returns Store.

Test consumer sweeps (~13 test files)
- tests/restapi/contacts/{test_contact,test_organization,test_employee,
  test_member}.py: attribute access throughout. Bulk update flows use
  model_dump(by_alias=True) before mutating. test_contact_get_not_found
  and test_organization_cycle catch ValidationError for null bodies.
- tests/restapi/pricing/{test_pricelist,test_assignments,test_prices}.py:
  same.
- tests/restapi/marketing/test_promotions.py: same. Server-only fields
  (description) read via reloaded.model_extra.
- tests/restapi/store/test_store.py: same.

Verified
- pytest --collect-only on tests/restapi/: 295 tests collected
- Live run on vcptcore-demo: 286 passed, 5 failed
  - 5 failures are the pre-existing personalization async-index issue
    (same baseline as before this PR's earlier commits)
  - Zero new regressions

Tier 2 (settings, notifications, oauth, api_key, content/cms, dynamic
content sub-types) and Tier 3 (health checks, dashboards) intentionally
not migrated — low typing payoff per file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 28, 2026

REST API Test Results

295 tests   292 ✅  26s ⏱️
  4 suites    3 💤
  4 files      0 ❌

Results for commit 5ed191b.

♻️ This comment has been updated with latest results.

@Lenajava1 Lenajava1 merged commit 26c629c into dev Apr 28, 2026
3 checks passed
@Lenajava1 Lenajava1 deleted the fix/restapi-pydantic-types branch April 28, 2026 18:51
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