diff --git a/mpt_api_client/models/model.py b/mpt_api_client/models/model.py index 321a63c0..c87c67d0 100644 --- a/mpt_api_client/models/model.py +++ b/mpt_api_client/models/model.py @@ -15,8 +15,7 @@ class MptBox(Box): """python-box that preserves camelCase keys when converted to json.""" def __init__(self, *args, attribute_mapping: dict[str, str] | None = None, **_): # type: ignore[no-untyped-def] - attribute_mapping = attribute_mapping or {} - self._attribute_mapping = attribute_mapping + self._attribute_mapping = (attribute_mapping or {}).copy() super().__init__( *args, camel_killer_box=False, @@ -34,7 +33,8 @@ def __setattr__(self, item: str, value: Any) -> None: if item in _box_safe_attributes: return object.__setattr__(self, item, value) - super().__setattr__(item, value) # type: ignore[no-untyped-call] + mapped_key = self._prep_key(item) + super().__setattr__(mapped_key, value) # type: ignore[no-untyped-call] return None @override @@ -50,7 +50,7 @@ def to_dict(self) -> dict[str, Any]: # noqa: WPS210 } out_dict = {} for parsed_key, item_value in super().to_dict().items(): - original_key = reverse_mapping[parsed_key] + original_key = reverse_mapping.get(parsed_key, parsed_key) out_dict[original_key] = item_value return out_dict @@ -58,8 +58,12 @@ def _prep_key(self, key: str) -> str: try: return self._attribute_mapping[key] except KeyError: - self._attribute_mapping[key] = _camel_killer(key) - return self._attribute_mapping[key] + # Check if key is already a value in the mapping (it's already the API key) + if key in self._attribute_mapping.values(): + return key + mapped_key = _camel_killer(key) + self._attribute_mapping[key] = mapped_key + return mapped_key # type: ignore[no-any-return] class Model: # noqa: WPS214 @@ -96,6 +100,12 @@ def __setattr__(self, attribute: str, attribute_value: Any) -> None: object.__setattr__(self, attribute, attribute_value) return + # Respect descriptors (e.g., @property setters) defined on the class + cls_attr = getattr(self.__class__, attribute, None) + if isinstance(cls_attr, property) and cls_attr.fset is not None: + cls_attr.fset(self, attribute_value) + return + self._box.__setattr__(attribute, attribute_value) @classmethod diff --git a/mpt_api_client/resources/catalog/products_parameters.py b/mpt_api_client/resources/catalog/products_parameters.py index 6205801e..57a19587 100644 --- a/mpt_api_client/resources/catalog/products_parameters.py +++ b/mpt_api_client/resources/catalog/products_parameters.py @@ -1,3 +1,5 @@ +from typing import Any, ClassVar + from mpt_api_client.http import AsyncService, Service from mpt_api_client.http.mixins import ( AsyncCollectionMixin, @@ -11,6 +13,108 @@ class Parameter(Model): """Parameter resource.""" + _attribute_mapping: ClassVar[dict[str, str]] = {"externalId": "external_id"} + + @property + def type_(self) -> str: + """Returns the parameter type.""" + return str(self._box.get("type", "")) # type: ignore[no-untyped-call] + + @type_.setter + def type_(self, value: str) -> None: + """Sets the parameter type.""" + self._box.type = value + + @property + def scope(self) -> str: + """Returns the parameter scope.""" + return str(self._box.get("scope", "")) # type: ignore[no-untyped-call] + + @scope.setter + def scope(self, value: str) -> None: + """Sets the parameter scope.""" + self._box.scope = value + + @property + def phase(self) -> str: + """Returns the parameter phase.""" + return str(self._box.get("phase", "")) # type: ignore[no-untyped-call] + + @phase.setter + def phase(self, value: str) -> None: + """Sets the parameter phase.""" + self._box.phase = value + + @property + def context(self) -> str: + """Returns the parameter context.""" + return str(self._box.get("context", "")) # type: ignore[no-untyped-call] + + @context.setter + def context(self, value: str) -> None: + """Sets the parameter context.""" + self._box.context = value + + @property + def options(self) -> dict[str, Any]: + """Returns the parameter options.""" + return self._box.get("options", {}) # type: ignore[no-any-return, no-untyped-call] + + @options.setter + def options(self, value: dict[str, Any]) -> None: + """Sets the parameter options.""" + self._box.options = value + + @property + def multiple(self) -> bool: + """Returns whether the parameter allows multiple values.""" + return bool(self._box.get("multiple", False)) # type: ignore[no-untyped-call] + + @multiple.setter + def multiple(self, value: bool) -> None: + """Sets whether the parameter allows multiple values.""" + self._box.multiple = value + + @property + def constraints(self) -> dict[str, Any]: + """Returns the parameter constraints.""" + return self._box.get("constraints", {}) # type: ignore[no-any-return, no-untyped-call] + + @constraints.setter + def constraints(self, value: dict[str, Any]) -> None: + """Sets the parameter constraints.""" + self._box.constraints = value + + @property + def group(self) -> dict[str, Any]: + """Returns the parameter group.""" + return self._box.get("group", {}) # type: ignore[no-any-return,no-untyped-call] + + @group.setter + def group(self, value: dict[str, Any]) -> None: + """Sets the parameter group.""" + self._box.group = value + + @property + def external_id(self) -> str: + """Returns the parameter external ID.""" + return str(self._box.get("external_id", "")) # type: ignore[no-untyped-call] + + @external_id.setter + def external_id(self, value: str) -> None: + """Sets the parameter external ID.""" + self._box.external_id = value + + @property + def status(self) -> str: + """Returns the parameter status.""" + return str(self._box.get("status", "")) # type: ignore[no-untyped-call] + + @status.setter + def status(self, value: str) -> None: + """Sets the parameter status.""" + self._box.status = value + class ParametersServiceConfig: """Parameters service configuration.""" diff --git a/tests/unit/resources/catalog/test_parameter_properties.py b/tests/unit/resources/catalog/test_parameter_properties.py new file mode 100644 index 00000000..05efad40 --- /dev/null +++ b/tests/unit/resources/catalog/test_parameter_properties.py @@ -0,0 +1,76 @@ +from mpt_api_client.resources.catalog.products_parameters import Parameter + + +def test_parameter_properties_getters(): # noqa: WPS218 + parameter_data = { + "id": "PAR-1", + "scope": "Order", + "phase": "Order", + "context": "Purchase", + "options": {"label": "Email"}, + "multiple": True, + "constraints": {"required": True}, + "group": {"id": "GRP-1"}, + "externalId": "ext-1", + "status": "Active", + } + + result = Parameter(parameter_data) + + assert result.id == "PAR-1" + assert result.scope == "Order" + assert result.phase == "Order" + assert result.context == "Purchase" + assert result.options == {"label": "Email"} + assert result.multiple is True + assert result.constraints == {"required": True} + assert result.group == {"id": "GRP-1"} + assert result.external_id == "ext-1" + assert result.status == "Active" + + +def test_parameter_properties_setters(): # noqa: WPS218 + parameter = Parameter() + parameter.scope = "Agreement" + parameter.phase = "Configuration" + parameter.context = "None" + parameter.type_ = "Text" + parameter.options = {"label": "Name"} + parameter.multiple = False + parameter.constraints = {"required": False} + parameter.group = {"id": "GRP-2"} + parameter.external_id = "ext-2" + parameter.status = "Inactive" + + result = parameter + + assert result.scope == "Agreement" + assert result.phase == "Configuration" + assert result.context == "None" + assert result.type_ == "Text" + assert result.options == {"label": "Name"} + assert result.multiple is False + assert result.constraints == {"required": False} + assert result.group == {"id": "GRP-2"} + assert result.external_id == "ext-2" + assert result.status == "Inactive" + result_dict = result.to_dict() + assert result_dict["scope"] == "Agreement" + assert result_dict["externalId"] == "ext-2" + assert result_dict["scope"] == "Agreement" + assert result_dict["externalId"] == "ext-2" + + +def test_parameter_default_values(): # noqa: WPS218 + result = Parameter() + + assert not result.id + assert not result.scope + assert not result.phase + assert not result.context + assert result.options == {} + assert result.multiple is False + assert result.constraints == {} + assert result.group == {} + assert not result.external_id + assert not result.status