Skip to content

Commit b1cb96d

Browse files
committed
WIP Expanding python-box
1 parent d13d35c commit b1cb96d

2 files changed

Lines changed: 70 additions & 3 deletions

File tree

mpt_api_client/models/model.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,68 @@
11
from typing import Any, ClassVar, Self, override
22

33
from box import Box
4+
from box.box import _camel_killer # type: ignore[attr-defined] # noqa: PLC2701
45

56
from mpt_api_client.http.types import Response
67
from mpt_api_client.models.meta import Meta
78

89
ResourceData = dict[str, Any]
910

1011

12+
class MptBox(Box):
13+
"""python-box that preserves camelCase keys when converted to json."""
14+
15+
def __init__(self, *args, key_mapping: dict[str, str] | None, **kwargs): # type: ignore[no-untyped-def]
16+
super().__init__(*args, **kwargs)
17+
key_mapping = key_mapping or {}
18+
if self._box_config.get("key_mapping") is None:
19+
self._box_config["key_mapping"] = key_mapping
20+
else:
21+
self._box_config.get("key_mapping").update(key_mapping)
22+
23+
@override
24+
def __setitem__(self, key, value): # type: ignore[no-untyped-def] # noqa: WPS110
25+
try:
26+
mapped_key = self._box_config["key_mapping"][key]
27+
except KeyError as error:
28+
if key == "key_mapping" and "key_mapping" in self._box_config:
29+
return
30+
if error.args[0] == "key_mapping" and "key_mapping" not in self._box_config:
31+
self._box_config["key_mapping"] = self._box_config.get("default_key_mappings", {})
32+
33+
mapped_key = _camel_killer(key)
34+
self._box_config["key_mapping"][key] = mapped_key
35+
super().__setitem__(mapped_key, value) # type: ignore[no-untyped-call]
36+
37+
@override
38+
def to_dict(self) -> dict[str, Any]: # noqa: WPS210
39+
reverse_mapping = {
40+
mapped_key: original_key
41+
for original_key, mapped_key in self._box_config.get("key_mapping", {}).items()
42+
}
43+
out_dict = {}
44+
for parsed_key, item_value in super().to_dict().items():
45+
original_key = reverse_mapping[parsed_key]
46+
out_dict[original_key] = item_value
47+
return out_dict
48+
49+
1150
class Model: # noqa: WPS214
1251
"""Provides a resource to interact with api data using fluent interfaces."""
1352

1453
_data_key: ClassVar[str | None] = None
1554
_safe_attributes: ClassVar[list[str]] = ["meta", "_box"]
55+
_case_mappings: ClassVar[dict[str, str]] = {}
1656

1757
def __init__(self, resource_data: ResourceData | None = None, meta: Meta | None = None) -> None:
1858
self.meta = meta
19-
self._box = Box(resource_data or {}, camel_killer_box=False, default_box=False)
59+
self._box = MptBox(
60+
resource_data or {},
61+
camel_killer_box=False,
62+
default_box=False,
63+
default_box_create_on_get=False,
64+
key_mapping=self._case_mappings,
65+
)
2066

2167
@classmethod
2268
def new(cls, resource_data: ResourceData | None = None, meta: Meta | None = None) -> Self:

tests/unit/models/resource/test_resource.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,31 @@ def test_case_conversion():
7777

7878
resource = Model(resource_data)
7979

80-
assert resource.FullName == "Alice Smith"
80+
assert resource.full_name == "Alice Smith"
8181
assert resource.to_dict() == resource_data
8282
with pytest.raises(AttributeError):
83-
resource.full_name # noqa: B018
83+
_ = resource.FullName # noqa: WPS122
84+
85+
86+
def test_deep_case_conversion():
87+
resource_data = {"id": "ABC-123", "contact": {"id": "ABC-345", "FullName": "Alice Smith"}}
88+
expected_resource_data = {
89+
"id": "ABC-123",
90+
"contact": {"id": "ABC-345", "FullName": "Alice Smith", "StreetAddress": "123 Main St"},
91+
}
92+
93+
resource = Model(resource_data)
94+
resource.contact.StreetAddress = "123 Main St"
95+
96+
assert resource.contact.full_name == "Alice Smith"
97+
assert resource.contact.street_address == "123 Main St"
98+
assert resource.to_dict() == expected_resource_data
99+
100+
with pytest.raises(AttributeError):
101+
_ = resource.contact.FullName # noqa: WPS122
102+
103+
with pytest.raises(AttributeError):
104+
_ = resource.contact.StreetAddress # noqa: WPS122
84105

85106

86107
def test_repr():

0 commit comments

Comments
 (0)