Skip to content

Commit aaca6a1

Browse files
authored
MPT-17888: add first-level field annotations to catalog service models (#231)
## 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. `PPx1` → `p_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`) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> Closes [MPT-17888](https://softwareone.atlassian.net/browse/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) <!-- end of auto-generated comment: release notes by coderabbit.ai --> [MPT-17888]: https://softwareone.atlassian.net/browse/MPT-17888?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2 parents 3d8b03f + 2c857dd commit aaca6a1

33 files changed

+1125
-20
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ cython_debug/
167167
.ruff_cache
168168
.idea
169169
.openapi/
170+
openapi/openapi-dev.json
171+
.github/copilot-instructions.md
170172
# Makefile
171173
make/local.mk
172174

mpt_api_client/models/model.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import re
22
from collections import UserList
33
from collections.abc import Iterable
4+
from types import MappingProxyType
45
from typing import Any, Self, get_args, get_origin, override
56

67
from mpt_api_client.http.types import Response
@@ -12,9 +13,47 @@
1213
_SNAKE_CASE_BOUNDARY = re.compile(r"([a-z0-9])([A-Z])")
1314
_SNAKE_CASE_ACRONYM = re.compile(r"(?<=[A-Z])(?=[A-Z][a-z0-9])")
1415

16+
# Explicit bidirectional mappings for API field names that contain two or more consecutive
17+
# uppercase letters (e.g. PPx1, unitLP). The generic regex cannot round-trip these correctly,
18+
# so we maintain an explicit lookup table that is checked before the regex is applied.
19+
_FIELD_NAME_MAPPINGS: MappingProxyType[str, str] = MappingProxyType({
20+
# PP* price columns
21+
"PPx1": "ppx1",
22+
"PPxM": "ppxm",
23+
"PPxY": "ppxy",
24+
# SP* price columns
25+
"SPx1": "spx1",
26+
"SPxM": "spxm",
27+
"SPxY": "spxy",
28+
# LP* price columns
29+
"LPx1": "lpx1",
30+
"LPxM": "lpxm",
31+
"LPxY": "lpxy",
32+
# unit + 2-letter acronym suffix
33+
"unitLP": "unit_lp",
34+
"unitPP": "unit_pp",
35+
"unitSP": "unit_sp",
36+
# total + 2-letter acronym suffix
37+
"totalGT": "total_gt",
38+
"totalPP": "total_pp",
39+
"totalSP": "total_sp",
40+
"totalST": "total_st",
41+
})
42+
43+
_FIELD_NAME_MAPPINGS_REVERSE: MappingProxyType[str, str] = MappingProxyType({
44+
snake: camel for camel, snake in _FIELD_NAME_MAPPINGS.items()
45+
})
46+
1547

1648
def to_snake_case(key: str) -> str:
17-
"""Converts a camelCase string to snake_case."""
49+
"""Converts a camelCase string to snake_case.
50+
51+
Explicit mappings in ``_FIELD_NAME_MAPPINGS`` take priority over the generic
52+
regex for fields that contain two or more consecutive uppercase letters.
53+
"""
54+
mapped = _FIELD_NAME_MAPPINGS.get(key)
55+
if mapped is not None:
56+
return mapped
1857
if "_" in key and key.islower():
1958
return key
2059
# Common pattern for PascalCase/camelCase conversion
@@ -24,7 +63,14 @@ def to_snake_case(key: str) -> str:
2463

2564

2665
def to_camel_case(key: str) -> str:
27-
"""Converts a snake_case string to camelCase."""
66+
"""Converts a snake_case string to camelCase.
67+
68+
Explicit mappings in ``_FIELD_NAME_MAPPINGS_REVERSE`` take priority over the
69+
generic logic for fields that contain two or more consecutive uppercase letters.
70+
"""
71+
mapped = _FIELD_NAME_MAPPINGS_REVERSE.get(key)
72+
if mapped is not None:
73+
return mapped
2874
parts = key.split("_")
2975
return parts[0] + "".join(x.title() for x in parts[1:]) # noqa: WPS111 WPS221
3076

mpt_api_client/resources/catalog/authorizations.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,37 @@
66
ManagedResourceMixin,
77
)
88
from mpt_api_client.models import Model
9+
from mpt_api_client.models.model import BaseModel
910

1011

1112
class Authorization(Model):
12-
"""Authorization resource."""
13+
"""Authorization resource.
14+
15+
Attributes:
16+
name: Authorization name.
17+
external_ids: External identifiers for the authorization.
18+
currency: Currency code associated with the authorization.
19+
notes: Additional notes.
20+
product: Reference to the product.
21+
vendor: Reference to the vendor.
22+
owner: Reference to the owner account.
23+
statistics: Authorization statistics.
24+
journal: Journal reference.
25+
eligibility: Eligibility information.
26+
audit: Audit information (created, updated events).
27+
"""
28+
29+
name: str | None
30+
external_ids: BaseModel | None
31+
currency: str | None
32+
notes: str | None
33+
product: BaseModel | None
34+
vendor: BaseModel | None
35+
owner: BaseModel | None
36+
statistics: BaseModel | None
37+
journal: BaseModel | None
38+
eligibility: BaseModel | None
39+
audit: BaseModel | None
1340

1441

1542
class AuthorizationsServiceConfig:

mpt_api_client/resources/catalog/items.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,41 @@
66
ManagedResourceMixin,
77
)
88
from mpt_api_client.models import Model
9+
from mpt_api_client.models.model import BaseModel
910
from mpt_api_client.resources.catalog.mixins import (
1011
AsyncPublishableMixin,
1112
PublishableMixin,
1213
)
1314

1415

1516
class Item(Model): # noqa: WPS110
16-
"""Item resource."""
17+
"""Item resource.
18+
19+
Attributes:
20+
name: Item name.
21+
description: Item description.
22+
external_ids: External identifiers for the item.
23+
group: Reference to the item group.
24+
unit: Reference to the unit of measure.
25+
terms: Reference to the terms and conditions.
26+
quantity_not_applicable: Whether quantity is not applicable to this item.
27+
status: Item status.
28+
product: Reference to the product.
29+
parameters: List of parameters associated with this item.
30+
audit: Audit information (created, updated events).
31+
"""
32+
33+
name: str | None
34+
description: str | None
35+
external_ids: BaseModel | None
36+
group: BaseModel | None
37+
unit: BaseModel | None
38+
terms: BaseModel | None
39+
quantity_not_applicable: bool | None
40+
status: str | None
41+
product: BaseModel | None
42+
parameters: list[BaseModel] | None
43+
audit: BaseModel | None
1744

1845

1946
class ItemsServiceConfig:

mpt_api_client/resources/catalog/listings.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,35 @@
66
ManagedResourceMixin,
77
)
88
from mpt_api_client.models import Model
9+
from mpt_api_client.models.model import BaseModel
910

1011

1112
class Listing(Model):
12-
"""Listing resource."""
13+
"""Listing resource.
14+
15+
Attributes:
16+
authorization: Reference to the authorization.
17+
product: Reference to the product.
18+
vendor: Reference to the vendor.
19+
seller: Reference to the seller.
20+
price_list: Reference to the associated price list.
21+
primary: Whether this is the primary listing.
22+
notes: Additional notes.
23+
statistics: Listing statistics.
24+
eligibility: Eligibility information.
25+
audit: Audit information (created, updated events).
26+
"""
27+
28+
authorization: BaseModel | None
29+
product: BaseModel | None
30+
vendor: BaseModel | None
31+
seller: BaseModel | None
32+
price_list: BaseModel | None
33+
primary: bool | None
34+
notes: str | None
35+
statistics: BaseModel | None
36+
eligibility: BaseModel | None
37+
audit: BaseModel | None
1338

1439

1540
class ListingsServiceConfig:

mpt_api_client/resources/catalog/price_list_items.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,55 @@
88
UpdateMixin,
99
)
1010
from mpt_api_client.models import Model
11+
from mpt_api_client.models.model import BaseModel
1112

1213

1314
class PriceListItem(Model):
14-
"""Price List Item resource."""
15+
"""Price List Item resource.
16+
17+
Attributes:
18+
status: Price list item status.
19+
description: Price list item description.
20+
reason_for_change: Reason for the price change.
21+
unit_lp: Unit list price.
22+
unit_pp: Unit purchase price.
23+
markup: Markup percentage.
24+
margin: Margin percentage.
25+
unit_sp: Unit sell price.
26+
ppx1: Purchase price for 1-year term.
27+
ppxm: Purchase price for monthly term.
28+
ppxy: Purchase price for yearly term.
29+
spx1: Sell price for 1-year term.
30+
spxm: Sell price for monthly term.
31+
spxy: Sell price for yearly term.
32+
lpx1: List price for 1-year term.
33+
lpxm: List price for monthly term.
34+
lpxy: List price for yearly term.
35+
price_list: Reference to the parent price list.
36+
item: Reference to the associated item.
37+
audit: Audit information (created, updated events).
38+
"""
39+
40+
status: str | None
41+
description: str | None
42+
reason_for_change: str | None
43+
unit_lp: float | None
44+
unit_pp: float | None
45+
markup: float | None
46+
margin: float | None
47+
unit_sp: float | None
48+
ppx1: float | None
49+
ppxm: float | None
50+
ppxy: float | None
51+
spx1: float | None
52+
spxm: float | None
53+
spxy: float | None
54+
lpx1: float | None
55+
lpxm: float | None
56+
lpxy: float | None
57+
price_list: BaseModel | None
58+
item: BaseModel | None
59+
audit: BaseModel | None
1560

1661

1762
class PriceListItemsServiceConfig:

mpt_api_client/resources/catalog/price_lists.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,39 @@
66
ManagedResourceMixin,
77
)
88
from mpt_api_client.models import Model
9+
from mpt_api_client.models.model import BaseModel
910
from mpt_api_client.resources.catalog.price_list_items import (
1011
AsyncPriceListItemsService,
1112
PriceListItemsService,
1213
)
1314

1415

1516
class PriceList(Model):
16-
"""Price List resource."""
17+
"""Price List resource.
18+
19+
Attributes:
20+
currency: Currency code for this price list.
21+
precision: Decimal precision for prices.
22+
default_markup: Default markup percentage.
23+
default_margin: Default margin percentage.
24+
notes: Additional notes.
25+
external_ids: External identifiers for the price list.
26+
statistics: Price list statistics.
27+
product: Reference to the associated product.
28+
vendor: Reference to the vendor.
29+
audit: Audit information (created, updated events).
30+
"""
31+
32+
currency: str | None
33+
precision: int | None
34+
default_markup: float | None
35+
default_margin: float | None
36+
notes: str | None
37+
external_ids: BaseModel | None
38+
statistics: BaseModel | None
39+
product: BaseModel | None
40+
vendor: BaseModel | None
41+
audit: BaseModel | None
1742

1843

1944
class PriceListsServiceConfig:

mpt_api_client/resources/catalog/pricing_policies.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,41 @@
99
ManagedResourceMixin,
1010
)
1111
from mpt_api_client.models import Model, ResourceData
12+
from mpt_api_client.models.model import BaseModel
1213
from mpt_api_client.resources.catalog.pricing_policy_attachments import (
1314
AsyncPricingPolicyAttachmentsService,
1415
PricingPolicyAttachmentsService,
1516
)
1617

1718

1819
class PricingPolicy(Model):
19-
"""Pricing policy resource."""
20+
"""Pricing policy resource.
21+
22+
Attributes:
23+
name: Pricing policy name.
24+
external_ids: External identifiers for the pricing policy.
25+
client: Reference to the client account.
26+
eligibility: Eligibility information.
27+
markup: Markup percentage.
28+
margin: Margin percentage.
29+
notes: Additional notes.
30+
products: List of associated products.
31+
status: Pricing policy status.
32+
statistics: Pricing policy statistics.
33+
audit: Audit information (created, updated events).
34+
"""
35+
36+
name: str | None
37+
external_ids: BaseModel | None
38+
client: BaseModel | None
39+
eligibility: BaseModel | None
40+
markup: float | None
41+
margin: float | None
42+
notes: str | None
43+
products: list[BaseModel] | None
44+
status: str | None
45+
statistics: BaseModel | None
46+
audit: BaseModel | None
2047

2148

2249
class PricingPoliciesServiceConfig:

mpt_api_client/resources/catalog/pricing_policy_attachments.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,31 @@
1010
ModifiableResourceMixin,
1111
)
1212
from mpt_api_client.models import Model
13+
from mpt_api_client.models.model import BaseModel
1314

1415

1516
class PricingPolicyAttachment(Model):
16-
"""Pricing Policy Attachment resource."""
17+
"""Pricing Policy Attachment resource.
18+
19+
Attributes:
20+
name: Attachment name.
21+
type: Attachment type.
22+
size: File size in bytes.
23+
description: Attachment description.
24+
file_name: Original file name.
25+
content_type: MIME content type of the attachment.
26+
status: Attachment status.
27+
audit: Audit information (created, updated events).
28+
"""
29+
30+
name: str | None
31+
type: str | None
32+
size: int | None
33+
description: str | None
34+
file_name: str | None
35+
content_type: str | None
36+
status: str | None
37+
audit: BaseModel | None
1738

1839

1940
class PricingPolicyAttachmentsServiceConfig:

0 commit comments

Comments
 (0)