Skip to content

Commit fd29e0c

Browse files
committed
added support for aliases function for names
1 parent bf605a3 commit fd29e0c

File tree

7 files changed

+137
-10
lines changed

7 files changed

+137
-10
lines changed

datamodel/abstract.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class Meta:
4343
validate_assignment: bool = False
4444
as_objects: bool = False
4545
no_nesting: bool = False
46+
alias_function: Optional[Callable] = None
4647

4748

4849
def set_connection(cls, conn: Callable):
@@ -283,6 +284,13 @@ def __init__(cls, *args, **kwargs) -> None:
283284

284285
def __call__(cls, *args, **kwargs):
285286
# rename any kwargs that match an alias ONLY if there are aliases defined.
287+
alias_func = getattr(cls.Meta, "alias_function", None)
288+
if callable(alias_func):
289+
new_kwargs = {}
290+
for k, v in kwargs.items():
291+
new_k = alias_func(k)
292+
new_kwargs[new_k] = v
293+
kwargs = new_kwargs
286294
if cls.__aliases__:
287295
new_kwargs = {}
288296
for k, v in kwargs.items():
@@ -291,6 +299,5 @@ def __call__(cls, *args, **kwargs):
291299
new_kwargs[real_field] = v
292300
else:
293301
new_kwargs[k] = v
294-
else:
295-
new_kwargs = kwargs
296-
return super().__call__(*args, **new_kwargs)
302+
kwargs = new_kwargs
303+
return super().__call__(*args, **kwargs)

datamodel/aliases/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Aliases Functions:
2+
import re
3+
4+
_RE_FIRST_CAP = re.compile(r"(.)([A-Z][a-z]+)")
5+
_RE_ALL_CAPS = re.compile(r"([a-z0-9])([A-Z])")
6+
7+
8+
def to_snakecase(name: str) -> str:
9+
"""
10+
Convert a CamelCase or PascalCase string into snake_case.
11+
Example: "EmailAddress" -> "email_address"
12+
"""
13+
import re
14+
# Insert underscores before capital letters, then lower-case
15+
s1 = _RE_FIRST_CAP.sub(r"\1_\2", name)
16+
return _RE_ALL_CAPS.sub(r"\1_\2", s1).lower()

datamodel/models.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,7 @@ def __convert_enums__(self, obj: Any) -> dict[str, Any]:
170170
key: self.__convert_enums__(value) for key, value in obj.items()
171171
}
172172
else:
173-
if isinstance(obj, Enum):
174-
return obj.value
175-
return obj
173+
return obj.value if isinstance(obj, Enum) else obj
176174

177175
def to_dict(
178176
self,
@@ -185,7 +183,7 @@ def to_dict(
185183
d = as_dict(self, dict_factory=dict)
186184
if convert_enums:
187185
d = self.__convert_enums__(d)
188-
if self.Meta.remove_nulls is True or remove_nulls is True:
186+
if self.Meta.remove_nulls is True or remove_nulls:
189187
return self.remove_nulls(d)
190188
# 4) If as_values => convert sub-models to pk-value
191189
return d

datamodel/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
__title__ = 'python-datamodel'
55
__description__ = ('simple library based on python +3.8 to use Dataclass-syntax'
66
'for interacting with Data')
7-
__version__ = '0.8.7'
7+
__version__ = '0.8.8'
88
__author__ = 'Jesus Lara'
99
__author_email__ = '[email protected]'
1010
__license__ = 'BSD'

examples/test_alias.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,55 @@
1+
from typing import List, Optional
12
from datamodel import BaseModel, Field
23

4+
data = {
5+
'store_name': "0002 Conn's HomePlus - Winston NC",
6+
'store_address': '3925 Oxford Station Way',
7+
'city': 'Winston-Salem', 'zipcode': '27103', 'phone_number': None,
8+
'emailAddress': "[email protected]",
9+
'store_number': None, 'store_status': 't',
10+
'latitude': '36.068799', 'longitude': '-80.328457',
11+
'timezone': 'America/New_York', 'account_id': 24, 'country_id': 'USA',
12+
'store_type': 'FreeStanding', 'account_name': "Conn's",
13+
'store_id': 24252, 'visit_rule': [],
14+
'visit_category': [],
15+
'client_name': ['HISENSE', 'FLEX-ROC', 'ASSEMBLY'],
16+
'market_name': '10', 'region_name': 'Assembly - Region',
17+
'district_name': 'Assembly - District',
18+
'org_name': 'assembly'
19+
}
20+
21+
# class Store(BaseModel):
22+
# email_address: str = Field(alias="emailAddress") # The user sees "emailAddress"
323

424
class Store(BaseModel):
5-
email_address: str = Field(alias="emailAddress") # The user sees "emailAddress"
25+
org_name: str = Field(required=True)
26+
store_id: int = Field(primary_key=True, required=True)
27+
store_name: str = Field(required=True)
28+
store_address: str
29+
city: str
30+
zipcode: str
31+
phone_number: Optional[str]
32+
email_address: str = Field(alias="emailAddress")
33+
store_number: Optional[str]
34+
store_status: str
35+
latitude: float
36+
longitude: float
37+
timezone: str
38+
account_id: int
39+
country_id: str
40+
store_type: str
41+
account_name: str
42+
visit_rule: List[str]
43+
visit_category: List[str]
44+
client_name: List[str]
45+
market_name: str
46+
region_name: str
47+
district_name: str
48+
49+
class Meta:
50+
strict = True
51+
as_objects = True
652

7-
store = Store(emailAddress="[email protected]")
53+
store = Store(**data)
854
print('Store > ', store)
955
print(store.email_address) # "[email protected]"

examples/test_aliases.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from typing import List, Optional
2+
from datamodel import BaseModel, Field
3+
from datamodel.aliases import to_snakecase
4+
5+
6+
class Store(BaseModel):
7+
email_address: str
8+
status: str
9+
store_id: int = Field(primary_key=True)
10+
11+
class Meta:
12+
strict = True
13+
as_objects = True
14+
alias_function = to_snakecase
15+
16+
# Example Usage:
17+
store = Store(EmailAddress="[email protected]", Status="ACTIVE", StoreId=1)
18+
print(store)

tests/test_aliases.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# tests/test_alias_function.py
2+
import pytest
3+
from datamodel import BaseModel, Field
4+
from datamodel.aliases import to_snakecase
5+
6+
class Store(BaseModel):
7+
# Fields (with normal pythonic names)
8+
email_address: str
9+
status: str
10+
store_id: int = Field(primary_key=True)
11+
12+
class Meta:
13+
strict = True
14+
as_objects = True
15+
# The function that transforms keys (e.g., EmailAddress -> email_address)
16+
alias_function = to_snakecase
17+
18+
class User(BaseModel):
19+
email_address: str = Field(alias='emailAddress')
20+
21+
def test_alias_function_store():
22+
"""Ensure that the alias_function (to_snakecase) properly converts keys."""
23+
# We pass in uppercase/camelcase versions of the fields:
24+
store = Store(emailAddress="[email protected]", Status="ACTIVE", StoreId=123)
25+
# Now we verify that the model's actual attributes got populated correctly:
26+
assert store.email_address == "[email protected]"
27+
assert store.status == "ACTIVE"
28+
assert store.store_id == 123
29+
30+
# Check the model's type and printing
31+
assert isinstance(store, Store)
32+
print(f"Created store: {store}")
33+
34+
35+
def test_alias_simple_store():
36+
"""Ensure that Alias on Field is properly managed."""
37+
# We pass in uppercase/camelcase versions of the fields:
38+
user = User(emailAddress="Test@Test")
39+
assert user.email_address == "Test@Test"
40+
41+
# Check the model's type and printing
42+
assert isinstance(user, User)

0 commit comments

Comments
 (0)