Skip to content

Commit 94aeaaa

Browse files
committed
extend ModelMixin.
1 parent f4b5994 commit 94aeaaa

File tree

6 files changed

+93
-6
lines changed

6 files changed

+93
-6
lines changed

.python-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.12
1+
3.14

CHANGES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## Version 4.2.0
2+
3+
* Add method `.to_dict()` to `ModelMixin`.
4+
* Add validate UTC timezone for datetime field in `ModelMixine`.
5+
16
## Version 4.1.0
27

38
* Add bulk methods to `CreateMixin`, `ReadMixin`, `UpdateMixin` and `DeleteMixin` mixins.

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ format: venv
2323

2424
test: venv
2525
# Run pytest.
26-
./venv/bin/pytest -x --cov-report term-missing:skip-covered --cov=db_first tests/
26+
./venv/bin/pytest -x --cov-report term-missing:skip-covered --cov=src/db_first tests/
2727

2828
tox: venv
2929
# Testing project via several Python versions.

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@ license = {file = "LICENSE"}
2525
name = "DB-First"
2626
readme = "README.md"
2727
requires-python = ">=3.9"
28-
version = "4.1.0"
28+
version = "4.2.0"
2929

3030
[project.optional-dependencies]
3131
dev = [
3232
"build==1.3.0",
3333
"pre-commit==4.3.0",
3434
"pytest==8.4.2",
3535
"pytest-cov==7.0.0",
36-
"python-dotenv==1.1.1",
36+
"python-dotenv==1.2.1",
3737
"tox==4.30.3",
3838
"twine==6.2.0"
3939
]

src/db_first/base_model.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import uuid
22
from datetime import datetime
3+
from datetime import timezone
34
from typing import Optional
45

6+
from sqlalchemy import DateTime
57
from sqlalchemy.orm import Mapped
68
from sqlalchemy.orm import mapped_column
9+
from sqlalchemy.orm import validates
710

811

912
def make_uuid4() -> uuid.UUID:
@@ -13,10 +16,56 @@ def make_uuid4() -> uuid.UUID:
1316
class ModelMixin:
1417
"""Mixin for table model."""
1518

19+
EMPTY_VALUES: tuple[None, list, dict, tuple] = (None, [], {}, ())
20+
1621
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=make_uuid4, comment='UUID')
1722
created_at: Mapped[datetime] = mapped_column(
18-
default=datetime.utcnow, comment='Date and time created'
23+
DateTime(timezone=True), default=datetime.utcnow, comment='Date and time created'
1924
)
2025
updated_at: Mapped[Optional[datetime]] = mapped_column(
21-
onupdate=datetime.utcnow, comment='Date and time updated'
26+
DateTime(timezone=True), onupdate=datetime.utcnow, comment='Date and time updated'
2227
)
28+
29+
@staticmethod
30+
def validate_utc_timezone(key: str, value: datetime or None) -> datetime:
31+
time_zone = getattr(value, 'tzinfo', None)
32+
if time_zone != timezone.utc:
33+
raise ValueError(
34+
f'Field <{key}> must be datetime with UTC timezone,'
35+
f' but received timezone: <{time_zone}>'
36+
)
37+
38+
return value
39+
40+
@validates('created_at', 'updated_at')
41+
def validate_timezone(self, key, value) -> datetime:
42+
return self.validate_utc_timezone(key, value)
43+
44+
def to_dict(self, fields: dict[str, dict or list or Ellipsis]):
45+
data = {}
46+
for field, value in fields.items():
47+
value_from_db = None
48+
49+
if value is Ellipsis:
50+
value_from_db = getattr(self, field)
51+
52+
elif isinstance(value, dict):
53+
obj = getattr(self, field)
54+
if obj:
55+
value_from_db = obj.to_dict(value)
56+
57+
elif isinstance(value, list):
58+
value_from_db = []
59+
for obj in getattr(self, field):
60+
for item in value:
61+
dictable_value_from_db = obj.to_dict(item)
62+
if dictable_value_from_db not in self.EMPTY_VALUES:
63+
value_from_db.append(dictable_value_from_db)
64+
65+
else:
66+
value_from_db = self.to_dict(value)
67+
68+
if value_from_db not in self.EMPTY_VALUES:
69+
data[field] = value_from_db
70+
71+
return data

tests/test_model_mixin.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from datetime import datetime
2+
3+
import pytest
4+
5+
6+
def test_model_mixin__to_dict(fx_parents__non_deletion):
7+
new_parent = fx_parents__non_deletion()
8+
9+
assert new_parent.to_dict(
10+
{
11+
'id': ...,
12+
'first': ...,
13+
'father': {'id': ..., 'first': ...},
14+
'children': [{'id': ..., 'first': ...}],
15+
}
16+
) == {
17+
'id': new_parent.id,
18+
'first': new_parent.first,
19+
'father': {'first': new_parent.father.first, 'id': new_parent.father.id},
20+
'children': [{'first': new_parent.children[0].first, 'id': new_parent.children[0].id}],
21+
}
22+
23+
24+
@pytest.mark.parametrize('date_time', ['', None, 'not_date_time', datetime.now()])
25+
def test_model_mixin__validate_timezone(fx_db, date_time):
26+
session, _, _, Fathers = fx_db
27+
28+
with pytest.raises(ValueError) as e:
29+
Fathers(first='first', created_at=date_time)
30+
31+
assert e.value.args[0] == (
32+
'Field <created_at> must be datetime with UTC timezone, but received timezone: <None>'
33+
)

0 commit comments

Comments
 (0)