Skip to content

MPT-17888: add first-level field annotations to catalog service models#231

Merged
d3rky merged 2 commits intomainfrom
MPT-17886_catalog_resources
Mar 13, 2026
Merged

MPT-17888: add first-level field annotations to catalog service models#231
d3rky merged 2 commits intomainfrom
MPT-17886_catalog_resources

Conversation

@albertsola
Copy link
Contributor

@albertsola albertsola commented Mar 13, 2026

Summary

Add typed field annotations to all 15 remaining catalog service model classes in mpt_api_client/resources/catalog/, following the same pattern established for Parameter and ParameterGroup in #230.

All fields are typed as T | None since RQL select can exclude any field from the API response.

Models updated

  • Authorization — 11 fields
  • Item — 11 fields
  • Listing — 10 fields
  • PriceList — 10 fields
  • PriceListItem — 20 fields (including price columns with non-standard casing)
  • PricingPolicy — 11 fields
  • PricingPolicyAttachment — 8 fields
  • Term — 6 fields
  • TermVariant — 12 fields
  • Document — 11 fields
  • ItemGroup — 10 fields
  • Media — 11 fields
  • Template — 6 fields
  • Product — 11 fields
  • UnitOfMeasure — 4 fields

Tests

Each model's test file was extended with:

  • Fixture with representative API response data
  • Round-trip test via to_dict()
  • Nested fields returning BaseModel instances
  • Absent fields verified with not hasattr() (model raises AttributeError for absent fields)

Notes

  • PriceListItem price columns use non-standard casing (e.g. PPx1p_px1) due to how to_snake_case works — round-trip via to_dict() is lossy for these fields, so only attribute-access tests are used for the price column group
  • list[BaseModel] | None used for array-of-object fields (Item.parameters, PricingPolicy.products)

Closes MPT-17888

  • Added typed first-level field annotations (T | None) to 15 catalog service model classes: Authorization, Item, Listing, PriceList, PriceListItem, PricingPolicy, PricingPolicyAttachment, Term, TermVariant, Document, ItemGroup, Media, Template, Product, and UnitOfMeasure
  • All fields marked as optional (| None) to reflect RQL select behavior where fields can be excluded from API responses
  • Implemented bidirectional field name mapping via _FIELD_NAME_MAPPINGS to handle API fields with consecutive uppercase letters (e.g., PPx1 → ppx1, unitLP → unit_lp), enabling lossless round-trip serialization
  • Updated case conversion utilities (to_snake_case and to_camel_case) to prioritize explicit field mappings over regex-based transformations
  • Extended test suite with representative API fixtures for each model and comprehensive test coverage including primitive field round-trips, nested BaseModel instances, and absent field behavior
  • Added documentation via expanded class docstrings and a new Copilot instructions guide (.github/copilot-instructions.md)
  • All array-of-object fields properly typed as list[BaseModel] | None (e.g., Item.parameters, PricingPolicy.products)

Add typed field annotations to all 15 remaining catalog service model
classes, following the same pattern established for Parameter and
ParameterGroup. All fields are typed as T | None since RQL select can
exclude any field from the API response.

Models updated:
- Authorization, Item, Listing, PriceList, PriceListItem
- PricingPolicy, PricingPolicyAttachment, Term, TermVariant
- Document, ItemGroup, Media, Template, Product, UnitOfMeasure

Each corresponding test file extended with fixture data and tests for:
- Primitive field round-trip via to_dict()
- Nested fields returning BaseModel instances
- Absent fields raising AttributeError (tested with not hasattr)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@albertsola albertsola requested a review from a team as a code owner March 13, 2026 17:33
@albertsola albertsola requested review from jentyk and svazquezco March 13, 2026 17:33
Comment on lines +40 to +59
status: str | None
description: str | None
reason_for_change: str | None
unit_lp: float | None
unit_pp: float | None
markup: float | None
margin: float | None
unit_sp: float | None
p_px1: float | None
p_px_m: float | None
p_px_y: float | None
s_px1: float | None
s_px_m: float | None
s_px_y: float | None
l_px1: float | None
l_px_m: float | None
l_px_y: float | None
price_list: BaseModel | None
item: BaseModel | None
audit: BaseModel | None
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is not ideal way to transform the Pascal Case into snake case for those variables.

Probably we want to specify a map of variables so we can have better namings.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could specify a map for the given resource PriceListItem to map the json to attributes as we would like.

The solution will work only if the resource is loaded using PriceListItem, but if it is loaded as side/sub resource by RQL we will have non consisting mapping.

A better solution would be to keep a list of keywords and their mappings. Not great but better unified attribute naming.

@robcsegal any thoughts in here?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, a list of keywords and their mappings would better. Not sure of a better way right now.

@coderabbitai
Copy link

coderabbitai bot commented Mar 13, 2026

📝 Walkthrough

Walkthrough

This pull request introduces explicit field name mappings in the model base class and adds comprehensive public type-annotated attributes to 15 catalog resource models alongside corresponding test coverage verifying field behavior, serialization, and optional field handling.

Changes

Cohort / File(s) Summary
Documentation & Core Model Infrastructure
.github/copilot-instructions.md, mpt_api_client/models/model.py
Added copilot guidelines documenting architecture and conventions. Enhanced model base class with explicit bidirectional field name mappings (_FIELD_NAME_MAPPINGS, _FIELD_NAME_MAPPINGS_REVERSE) that prioritize over regex-based snake/camel case conversion.
Catalog Resource Model Expansions
mpt_api_client/resources/catalog/authorizations.py, items.py, listings.py, price_list_items.py, price_lists.py, pricing_policies.py, pricing_policy_attachments.py, product_term_variants.py, product_terms.py, products.py, products_documents.py, products_item_groups.py, products_media.py, products_templates.py, units_of_measure.py
Systematically expanded 15 resource model classes with public optional typed attributes (str | None, int | None, float | None, bool | None, BaseModel | None, list[BaseModel] | None) and descriptive docstrings. All changes follow consistent patterns: BaseModel import, attribute declarations with union types, and documentation updates.
Model Utility Tests
tests/unit/models/test_model.py
Added parametrized tests for consecutive uppercase handling in case conversion utilities (to_snake_case and to_camel_case).
Catalog Resource Model Tests
tests/unit/resources/catalog/test_authorizations.py, test_items.py, test_listings.py, test_price_list_items.py, test_price_lists.py, test_pricing_policies.py, test_pricing_policy_attachments.py, test_product_term_variants.py, test_product_terms.py, test_products.py, test_products_documents.py, test_products_item_groups.py, test_products_media.py, test_products_templates.py, test_units_of_measure.py
Added comprehensive test coverage for 15 resource models, each including fixture data, primitive field round-trip validation via to_dict(), nested BaseModel field type assertions, and optional field absence checks when constructing with minimal data.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Jira Issue Key In Title ✅ Passed The PR title contains exactly one Jira issue key in the correct format (MPT-17888).
Test Coverage Required ✅ Passed PR modifies 17 code files and includes 16 test files with corresponding test cases for each modified catalog model, meeting the requirement for test coverage alongside code modifications.
Single Commit Required ✅ Passed The PR contains exactly one commit (2c857dd) that consolidates all changes described in the PR objectives into a single logical unit.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

…letters

Add _FIELD_NAME_MAPPINGS (MappingProxyType) in model.py for API fields
that contain two or more consecutive uppercase letters (PPx1, SPxM,
unitLP, totalGT, etc.). The generic camelCase<->snake_case regex cannot
round-trip these correctly, so an explicit lookup table is checked first
in both to_snake_case and to_camel_case.

Affected fields from OpenAPI spec:
- PP*/SP*/LP* price columns (PPx1→ppx1, SPxM→spxm, LPxY→lpxy, ...)
- unit+acronym fields (unitLP→unit_lp, unitPP→unit_pp, unitSP→unit_sp)
- total+acronym fields (totalGT→total_gt, totalPP→total_pp, ...)

PriceListItem model annotations updated to use the corrected names
(ppx1, spxm, lpx1, etc. instead of p_px1, s_px_m, l_px1, ...).
to_dict() round-trip now works correctly for all price columns.

Tests:
- Merged price_list_item price fixture into main fixture for full
  round-trip test coverage
- Added parametrized tests for to_snake_case and to_camel_case with
  consecutive-uppercase fields in tests/unit/models/test_model.py

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@albertsola albertsola force-pushed the MPT-17886_catalog_resources branch from 9485e54 to 2c857dd Compare March 13, 2026 18:11
@sonarqubecloud
Copy link

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
tests/unit/resources/catalog/test_pricing_policies.py (1)

191-199: Add explicit assertion for products list element coercion.

Since products is now part of the typed contract, it’s worth asserting list elements are BaseModel instances to lock in array-of-object behavior.

Suggested test addition
 def test_pricing_policy_nested_models(pricing_policy_data):
     result = PricingPolicy(pricing_policy_data)

     assert isinstance(result.external_ids, BaseModel)
     assert isinstance(result.client, BaseModel)
     assert isinstance(result.eligibility, BaseModel)
+    assert result.products is not None
+    assert isinstance(result.products[0], BaseModel)
     assert isinstance(result.statistics, BaseModel)
     assert isinstance(result.audit, BaseModel)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/resources/catalog/test_pricing_policies.py` around lines 191 -
199, The test test_pricing_policy_nested_models should also assert that the
PricingPolicy.products field is coerced to a list of BaseModel instances: add
assertions that result.products is a list and that every element in
result.products satisfies isinstance(element, BaseModel) (e.g., using all(...)
or a loop) so the array-of-object behavior is locked in; locate the test
function test_pricing_policy_nested_models and the PricingPolicy construction to
add these checks.
tests/unit/resources/catalog/test_items.py (1)

79-86: Consider adding terms to nested model assertions.

The Item model has terms: BaseModel | None annotated, but this test only asserts on 5 of the 6 nested BaseModel fields. Adding terms would ensure complete coverage.

♻️ Suggested addition
     assert isinstance(result.external_ids, BaseModel)
     assert isinstance(result.group, BaseModel)
     assert isinstance(result.unit, BaseModel)
+    assert isinstance(result.terms, BaseModel)
     assert isinstance(result.product, BaseModel)
     assert isinstance(result.audit, BaseModel)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/resources/catalog/test_items.py` around lines 79 - 86, The test
test_item_nested_base_models is missing an assertion for the Item.terms field;
update the test to also verify that result.terms is either an instance of
BaseModel or None (i.e., assert isinstance(result.terms, BaseModel) or
result.terms is None) so the nested-field coverage for Item (the terms
attribute) is complete.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/unit/resources/catalog/test_items.py`:
- Around line 79-86: The test test_item_nested_base_models is missing an
assertion for the Item.terms field; update the test to also verify that
result.terms is either an instance of BaseModel or None (i.e., assert
isinstance(result.terms, BaseModel) or result.terms is None) so the nested-field
coverage for Item (the terms attribute) is complete.

In `@tests/unit/resources/catalog/test_pricing_policies.py`:
- Around line 191-199: The test test_pricing_policy_nested_models should also
assert that the PricingPolicy.products field is coerced to a list of BaseModel
instances: add assertions that result.products is a list and that every element
in result.products satisfies isinstance(element, BaseModel) (e.g., using
all(...) or a loop) so the array-of-object behavior is locked in; locate the
test function test_pricing_policy_nested_models and the PricingPolicy
construction to add these checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 09f15ab2-a36a-4d1a-a48c-9dbe338bfe31

📥 Commits

Reviewing files that changed from the base of the PR and between 3d8b03f and 9485e54.

📒 Files selected for processing (34)
  • .github/copilot-instructions.md
  • mpt_api_client/models/model.py
  • mpt_api_client/resources/catalog/authorizations.py
  • mpt_api_client/resources/catalog/items.py
  • mpt_api_client/resources/catalog/listings.py
  • mpt_api_client/resources/catalog/price_list_items.py
  • mpt_api_client/resources/catalog/price_lists.py
  • mpt_api_client/resources/catalog/pricing_policies.py
  • mpt_api_client/resources/catalog/pricing_policy_attachments.py
  • mpt_api_client/resources/catalog/product_term_variants.py
  • mpt_api_client/resources/catalog/product_terms.py
  • mpt_api_client/resources/catalog/products.py
  • mpt_api_client/resources/catalog/products_documents.py
  • mpt_api_client/resources/catalog/products_item_groups.py
  • mpt_api_client/resources/catalog/products_media.py
  • mpt_api_client/resources/catalog/products_templates.py
  • mpt_api_client/resources/catalog/units_of_measure.py
  • openapi/openapi-dev.json
  • tests/unit/models/test_model.py
  • tests/unit/resources/catalog/test_authorizations.py
  • tests/unit/resources/catalog/test_items.py
  • tests/unit/resources/catalog/test_listings.py
  • tests/unit/resources/catalog/test_price_list_items.py
  • tests/unit/resources/catalog/test_price_lists.py
  • tests/unit/resources/catalog/test_pricing_policies.py
  • tests/unit/resources/catalog/test_pricing_policy_attachments.py
  • tests/unit/resources/catalog/test_product_term_variants.py
  • tests/unit/resources/catalog/test_product_terms.py
  • tests/unit/resources/catalog/test_products.py
  • tests/unit/resources/catalog/test_products_documents.py
  • tests/unit/resources/catalog/test_products_item_groups.py
  • tests/unit/resources/catalog/test_products_media.py
  • tests/unit/resources/catalog/test_products_templates.py
  • tests/unit/resources/catalog/test_units_of_measure.py

@d3rky d3rky merged commit aaca6a1 into main Mar 13, 2026
4 checks passed
@d3rky d3rky deleted the MPT-17886_catalog_resources branch March 13, 2026 19:27
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.

3 participants