11import uuid
22from datetime import datetime
3+ from datetime import timezone
34from typing import Optional
45
6+ from sqlalchemy import DateTime
57from sqlalchemy .orm import Mapped
68from sqlalchemy .orm import mapped_column
9+ from sqlalchemy .orm import validates
710
811
912def make_uuid4 () -> uuid .UUID :
@@ -13,10 +16,56 @@ def make_uuid4() -> uuid.UUID:
1316class 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
0 commit comments