database-wrapper 0.1.44__py3-none-any.whl → 0.1.72__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,12 +6,15 @@ database_wrapper package - Base for database wrappers
6
6
 
7
7
  import logging
8
8
 
9
+ from .abc import ConnectionABC, CursorABC, CursorAsyncABC, ConnectionAsyncABC
9
10
  from . import utils
10
11
  from .db_backend import DatabaseBackend
11
- from .db_data_model import DBDataModel, DBDefaultsDataModel
12
+ from .db_data_model import MetadataDict, DBDataModel, DBDefaultsDataModel
12
13
  from .common import OrderByItem, DataModelType, NoParam
13
14
  from .db_wrapper import DBWrapper
14
15
  from .db_wrapper_async import DBWrapperAsync
16
+ from .serialization import SerializeType
17
+ from .utils.dataclass_addons import ignore_unknown_kwargs
15
18
 
16
19
  # Set the logger to a quiet default, can be enabled if needed
17
20
  logger = logging.getLogger("database_wrapper")
@@ -30,8 +33,16 @@ __all__ = [
30
33
  "DBWrapper",
31
34
  "DBWrapperAsync",
32
35
  # Helpers
36
+ "MetadataDict",
33
37
  "DataModelType",
34
38
  "OrderByItem",
35
39
  "NoParam",
36
40
  "utils",
41
+ "SerializeType",
42
+ "ignore_unknown_kwargs",
43
+ # Abstract classes
44
+ "ConnectionABC",
45
+ "CursorABC",
46
+ "CursorAsyncABC",
47
+ "ConnectionAsyncABC",
37
48
  ]
@@ -0,0 +1,68 @@
1
+ from typing import Dict, Any, Literal
2
+ from typing import Any, Protocol, Sequence
3
+
4
+ from psycopg.sql import Composable
5
+
6
+ Row = Dict[str, Any]
7
+
8
+
9
+ class CursorABC(Protocol):
10
+ def execute(
11
+ self,
12
+ operation: str | Composable,
13
+ parameters: Sequence[Any] | None = None,
14
+ ) -> None: ...
15
+ def executemany(
16
+ self,
17
+ operation: str | Composable,
18
+ seq_of_parameters: Sequence[Sequence[Any]],
19
+ ) -> None: ...
20
+ def fetchone(self) -> Row | None: ...
21
+ def fetchmany(self, size: int | None = None) -> list[Row]: ...
22
+ def fetchall(self) -> list[Row]: ...
23
+ def close(self) -> None: ...
24
+ def setinputsizes(self, sizes: Sequence[Any]) -> None: ...
25
+ def setoutputsize(self, size: Any, column: Any = None) -> None: ...
26
+
27
+ @property
28
+ def rowcount(self) -> int | Literal[-1]: ...
29
+ @property
30
+ def lastrowid(self) -> int | None: ...
31
+
32
+
33
+ class ConnectionABC(Protocol):
34
+ def cursor(self) -> CursorABC: ...
35
+ def commit(self) -> None: ...
36
+ def rollback(self) -> None: ...
37
+ def close(self) -> None: ...
38
+
39
+
40
+ class CursorAsyncABC(Protocol):
41
+ async def execute(
42
+ self,
43
+ operation: str | Composable,
44
+ parameters: Sequence[Any] | None = None,
45
+ ) -> None: ...
46
+ async def executemany(
47
+ self,
48
+ operation: str | Composable,
49
+ seq_of_parameters: Sequence[Sequence[Any]],
50
+ ) -> None: ...
51
+ async def fetchone(self) -> Row | None: ...
52
+ async def fetchmany(self, size: int | None = None) -> list[Row]: ...
53
+ async def fetchall(self) -> list[Row]: ...
54
+ async def close(self) -> None: ...
55
+ def setinputsizes(self, sizes: Sequence[Any]) -> None: ...
56
+ def setoutputsize(self, size: Any, column: Any = None) -> None: ...
57
+
58
+ @property
59
+ def rowcount(self) -> int | Literal[-1]: ...
60
+ @property
61
+ def lastrowid(self) -> int | None: ...
62
+
63
+
64
+ class ConnectionAsyncABC(Protocol):
65
+ async def cursor(self) -> CursorAsyncABC: ...
66
+ async def commit(self) -> None: ...
67
+ async def rollback(self) -> None: ...
68
+ async def close(self) -> None: ...
@@ -3,7 +3,7 @@ from typing import Any
3
3
  CONFIG: dict[str, Any] = {
4
4
  # These are supposed to be set automatically by a git pre-compile script
5
5
  # They are one git commit hash behind, if used automatically
6
- "git_commit_hash": "ec2eca45cd9798f0c045418602fbe07ed7734848",
7
- "git_commit_date": "15.11.2024 18:06",
8
- "app_version": "0.1.44",
6
+ "git_commit_hash": "943e0234156c4a164061e5af9fb9dbd8c3041f6f",
7
+ "git_commit_date": "26.11.2024 22:09",
8
+ "app_version": "0.1.72",
9
9
  }
@@ -5,63 +5,114 @@ from typing import Any
5
5
  from threading import Event
6
6
  from contextvars import ContextVar
7
7
 
8
- from .utils.timer import Timer
9
-
10
8
 
11
9
  class DatabaseBackend:
10
+ config: Any
11
+ """ Database configuration """
12
+
13
+ connectionTimeout: int
14
+ """ Connection timeout """
15
+
16
+ name: str
17
+ """ Instance name """
18
+
19
+ # TODO: This should be made to increase exponentially
20
+ slowDownTimeout: int
21
+ """ How long to wait before trying to reconnect """
22
+
23
+ pool: Any
24
+ """ Connection pool """
25
+
26
+ poolAsync: Any
27
+ """ Async connection pool """
28
+
12
29
  connection: Any
30
+ """ Connection to database """
31
+
13
32
  cursor: Any
33
+ """ Cursor to database """
34
+
14
35
  contextConnection: ContextVar[Any | None]
15
- contextAsyncConnection: ContextVar[Any | None]
36
+ """ Connection used in context manager """
16
37
 
17
- config: Any
38
+ contextConnectionAsync: ContextVar[Any | None]
39
+ """ Connection used in async context manager """
18
40
 
19
- connectionTimeout: int
20
- slowDownTimeout: int = 5
41
+ loggerName: str
42
+ """ Logger name """
21
43
 
22
- name: str
23
44
  logger: logging.Logger
24
- timer: ContextVar[Timer | None]
45
+ """ Logger """
25
46
 
26
47
  shutdownRequested: Event
48
+ """
49
+ Event to signal shutdown
50
+ Used to stop database pool from creating new connections
51
+ """
52
+
53
+ ########################
54
+ ### Class Life Cycle ###
55
+ ########################
27
56
 
28
57
  def __init__(
29
58
  self,
30
59
  dbConfig: Any,
31
60
  connectionTimeout: int = 5,
32
61
  instanceName: str = "database_backend",
62
+ slowDownTimeout: int = 5,
33
63
  ) -> None:
34
64
  """
35
65
  Main concept here is that in init we do not connect to database,
36
66
  so that class instances can be safely made regardless of connection statuss.
37
67
 
38
- Remember to call open() before using this class.
68
+ Remember to call open() or openPool() before using this class.
39
69
  Close will be called automatically when class is destroyed.
40
- But sometimes in async environment you should call close() proactively.
70
+
71
+ Contexts are not implemented here, but in child classes should be used
72
+ by using connection pooling.
73
+
74
+ Async classes should be called manually and should override __del__ method,
75
+ if not upon destroying the class, an error will be raised that method was not awaited.
41
76
  """
42
77
 
43
78
  self.config = dbConfig
44
79
  self.connectionTimeout = connectionTimeout
45
80
  self.name = instanceName
81
+ self.slowDownTimeout = slowDownTimeout
82
+
83
+ self.loggerName = f"{__name__}.{self.__class__.__name__}.{self.name}"
84
+ self.logger = logging.getLogger(self.loggerName)
46
85
 
47
- loggerName = f"{__name__}.{self.__class__.__name__}.{self.name}"
48
- self.logger = logging.getLogger(loggerName)
49
- self.timer = ContextVar(f"db_timer", default=None)
86
+ self.pool = None
87
+ self.poolAsync = None
50
88
 
51
89
  self.connection = None
52
90
  self.cursor = None
53
91
  self.shutdownRequested = Event()
54
92
  self.contextConnection = ContextVar(f"db_connection_{self.name}", default=None)
55
- self.contextAsyncConnection = ContextVar(
93
+ self.contextConnectionAsync = ContextVar(
56
94
  f"db_connection_{self.name}_async", default=None
57
95
  )
58
96
 
59
97
  def __del__(self) -> None:
60
98
  """What to do when class is destroyed"""
61
99
  self.logger.debug("Dealloc")
100
+
101
+ # Clean up connections
62
102
  self.close()
103
+ self.closePool()
104
+
105
+ # Clean just in case
106
+ del self.connection
107
+ del self.cursor
108
+
109
+ del self.pool
110
+ del self.poolAsync
111
+
112
+ ###############
113
+ ### Context ###
114
+ ###############
63
115
 
64
- # Context
65
116
  def __enter__(self) -> tuple[Any, Any]:
66
117
  """Context manager"""
67
118
  raise Exception("Not implemented")
@@ -78,16 +129,23 @@ class DatabaseBackend:
78
129
  """Context manager"""
79
130
  raise Exception("Not implemented")
80
131
 
81
- # Connection
82
- def open(self) -> None:
83
- """Connect to database"""
84
- raise Exception("Not implemented")
132
+ ##################
133
+ ### Connection ###
134
+ ##################
135
+
136
+ def openPool(self) -> Any:
137
+ """Open connection pool"""
138
+ ...
85
139
 
86
- async def openAsync(self) -> None:
140
+ def closePool(self) -> Any:
141
+ """Close connection pool"""
142
+ ...
143
+
144
+ def open(self) -> Any:
87
145
  """Connect to database"""
88
- raise Exception("Not implemented")
146
+ ...
89
147
 
90
- def close(self) -> None:
148
+ def close(self) -> Any:
91
149
  """Close connections"""
92
150
  if self.cursor:
93
151
  self.logger.debug("Closing cursor")
@@ -99,7 +157,34 @@ class DatabaseBackend:
99
157
  self.connection.close()
100
158
  self.connection = None
101
159
 
102
- def fixSocketTimeouts(self, fd: Any):
160
+ def newConnection(self) -> Any:
161
+ """
162
+ Create new connection
163
+
164
+ Used for async context manager and async connection creation
165
+
166
+ Returns:
167
+ tuple[Any, Any] | None: Connection and cursor
168
+ """
169
+ raise Exception("Not implemented")
170
+
171
+ def returnConnection(self, connection: Any) -> Any:
172
+ """
173
+ Return connection to pool
174
+
175
+ Used for async context manager and async connections return.
176
+ For example to return connection to a pool.
177
+
178
+ Args:
179
+ connection (Any): Connection to return to pool
180
+ """
181
+ raise Exception("Not implemented")
182
+
183
+ ###############
184
+ ### Helpers ###
185
+ ###############
186
+
187
+ def fixSocketTimeouts(self, fd: Any) -> None:
103
188
  # Lets do some socket magic
104
189
  s = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM)
105
190
  # Enable sending of keep-alive messages
@@ -118,32 +203,36 @@ class DatabaseBackend:
118
203
  socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, self.connectionTimeout * 1000
119
204
  )
120
205
 
121
- async def newConnection(
122
- self,
123
- ) -> tuple[Any, Any] | None:
124
- """
125
- Create new connection
206
+ ####################
207
+ ### Transactions ###
208
+ ####################
126
209
 
127
- Used for async context manager and async connection creation
210
+ def beginTransaction(self) -> Any:
211
+ """Start transaction"""
212
+ raise Exception("Not implemented")
128
213
 
129
- Returns:
130
- tuple[Any, Any] | None: Connection and cursor
131
- """
214
+ def commitTransaction(self) -> Any:
215
+ """Commit transaction"""
132
216
  raise Exception("Not implemented")
133
217
 
134
- async def returnConnection(self, connection: Any) -> None:
135
- """
136
- Return connection to pool
218
+ def rollbackTransaction(self) -> Any:
219
+ """Rollback transaction"""
220
+ raise Exception("Not implemented")
137
221
 
138
- Used for async context manager and async connections return.
139
- For example to return connection to a pool.
222
+ # @contextmanager
223
+ def transaction(self, dbConn: Any = None) -> Any:
224
+ """
225
+ Transaction context manager
140
226
 
141
- Args:
142
- connection (Any): Connection to return to pool
227
+ ! When overriding this method, remember to use context manager.
228
+ ! Its not defined here, so that it can be used in both sync and async methods.
143
229
  """
144
230
  raise Exception("Not implemented")
145
231
 
146
- # Data
232
+ ############
233
+ ### Data ###
234
+ ############
235
+
147
236
  def lastInsertId(self) -> int:
148
237
  """Get last inserted row id generated by auto increment"""
149
238
  raise Exception("Not implemented")
@@ -152,10 +241,10 @@ class DatabaseBackend:
152
241
  """Get affected rows count"""
153
242
  raise Exception("Not implemented")
154
243
 
155
- def commit(self) -> None:
244
+ def commit(self) -> Any:
156
245
  """Commit DB queries"""
157
246
  raise Exception("Not implemented")
158
247
 
159
- def rollback(self) -> None:
248
+ def rollback(self) -> Any:
160
249
  """Rollback DB queries"""
161
250
  raise Exception("Not implemented")
@@ -1,12 +1,30 @@
1
+ from enum import Enum
1
2
  import re
2
3
  import json
3
4
  import datetime
4
5
  import dataclasses
5
6
 
6
- from enum import Enum
7
7
  from dataclasses import dataclass, field, asdict
8
- from decimal import Decimal
9
- from typing import Any
8
+ from typing import Any, Callable, Literal, NotRequired, Type, TypeVar, TypedDict, cast
9
+
10
+ from .serialization import (
11
+ SerializeType,
12
+ deserializeValue,
13
+ jsonEncoder,
14
+ serializeValue,
15
+ )
16
+
17
+ EnumType = TypeVar("EnumType", bound=Enum)
18
+
19
+
20
+ class MetadataDict(TypedDict):
21
+ db_field: tuple[str, str]
22
+ store: bool
23
+ update: bool
24
+ exclude: NotRequired[bool]
25
+ serialize: NotRequired[Callable[[Any], Any] | SerializeType | None]
26
+ deserialize: NotRequired[Callable[[Any], Any] | None]
27
+ enum_class: NotRequired[Type[Enum] | None]
10
28
 
11
29
 
12
30
  @dataclass
@@ -37,12 +55,12 @@ class DBDataModel:
37
55
  - validate(): Validates the instance.
38
56
 
39
57
  To enable storing and updating fields that by default are not stored or updated, use the following methods:
40
- - setStore(field_name: str, enable: bool = True): Enable/Disable storing a field.
41
- - setUpdate(field_name: str, enable: bool = True): Enable/Disable updating a field.
58
+ - setStore(fieldName: str, enable: bool = True): Enable/Disable storing a field.
59
+ - setUpdate(fieldName: str, enable: bool = True): Enable/Disable updating a field.
42
60
 
43
61
  To exclude a field from the dictionary representation of the instance, set metadata key "exclude" to True.
44
62
  To change exclude status of a field, use the following method:
45
- - setExclude(field_name: str, enable: bool = True): Exclude a field from dict representation.
63
+ - setExclude(fieldName: str, enable: bool = True): Exclude a field from dict representation.
46
64
  """
47
65
 
48
66
  ######################
@@ -96,7 +114,7 @@ class DBDataModel:
96
114
  ### Conversion methods ###
97
115
  ##########################
98
116
 
99
- def fillDataFromDict(self, kwargs: dict[str, Any]):
117
+ def fillDataFromDict(self, kwargs: dict[str, Any]) -> None:
100
118
  fieldNames = set([f.name for f in dataclasses.fields(self)])
101
119
  for key in kwargs:
102
120
  if key in fieldNames:
@@ -105,12 +123,25 @@ class DBDataModel:
105
123
  self.__post_init__()
106
124
 
107
125
  # Init data
108
- def __post_init__(self):
109
- for field_name, field_obj in self.__dataclass_fields__.items():
110
- metadata = field_obj.metadata
111
- encode = metadata.get("encode", None)
112
- if encode is not None:
113
- setattr(self, field_name, encode(getattr(self, field_name)))
126
+ def __post_init__(self) -> None:
127
+ for fieldName, fieldObj in self.__dataclass_fields__.items():
128
+ metadata = cast(MetadataDict, fieldObj.metadata)
129
+ value = getattr(self, fieldName)
130
+
131
+ # If serialize is set, and serialize is a SerializeType,
132
+ # we use our serialization function
133
+ # Here we actually need to deserialize the value to correct class type
134
+ serialize = metadata.get("serialize", None)
135
+ enumClass = metadata.get("enum_class", None)
136
+ if serialize is not None and isinstance(serialize, SerializeType):
137
+ value = deserializeValue(value, serialize, enumClass)
138
+ setattr(self, fieldName, value)
139
+
140
+ else:
141
+ deserialize = metadata.get("deserialize", None)
142
+ if deserialize is not None:
143
+ value = deserialize(value)
144
+ setattr(self, fieldName, value)
114
145
 
115
146
  # String - representation
116
147
  def __repr__(self) -> str:
@@ -125,7 +156,7 @@ class DBDataModel:
125
156
  for field in pairs:
126
157
  classField = self.__dataclass_fields__.get(field[0], None)
127
158
  if classField is not None:
128
- metadata = classField.metadata
159
+ metadata = cast(MetadataDict, classField.metadata)
129
160
  if not "exclude" in metadata or not metadata["exclude"]:
130
161
  newDict[field[0]] = field[1]
131
162
 
@@ -145,32 +176,20 @@ class DBDataModel:
145
176
  "id": {"type": "number"},
146
177
  },
147
178
  }
148
- for field_name, field_obj in self.__dataclass_fields__.items():
149
- metadata = field_obj.metadata
179
+ for fieldName, fieldObj in self.__dataclass_fields__.items():
180
+ metadata = cast(MetadataDict, fieldObj.metadata)
150
181
  assert (
151
182
  "db_field" in metadata
152
183
  and isinstance(metadata["db_field"], tuple)
153
184
  and len(metadata["db_field"]) == 2
154
- ), f"db_field metadata is not set for {field_name}"
185
+ ), f"db_field metadata is not set for {fieldName}"
155
186
  fieldType: str = metadata["db_field"][1]
156
- schema["properties"][field_name] = {"type": fieldType}
187
+ schema["properties"][fieldName] = {"type": fieldType}
157
188
 
158
189
  return schema
159
190
 
160
191
  def jsonEncoder(self, obj: Any) -> Any:
161
- if isinstance(obj, Decimal):
162
- return float(obj)
163
-
164
- if isinstance(obj, datetime.date) or isinstance(obj, datetime.datetime):
165
- return obj.strftime("%Y-%m-%dT%H:%M:%S")
166
-
167
- if isinstance(obj, Enum):
168
- return obj.value
169
-
170
- if isinstance(obj, int) or isinstance(obj, float) or isinstance(obj, str):
171
- return obj
172
-
173
- return str(obj)
192
+ return jsonEncoder(obj)
174
193
 
175
194
  def toJsonString(self, pretty: bool = False) -> str:
176
195
  if pretty:
@@ -227,35 +246,47 @@ class DBDataModel:
227
246
 
228
247
  return 0
229
248
 
230
- def validate(self) -> bool:
249
+ def validate(self) -> Literal[True] | str:
250
+ """
251
+ True if the instance is valid, otherwise an error message.
252
+ """
231
253
  raise NotImplementedError("`validate` is not implemented")
232
254
 
233
- def setStore(self, field_name: str, enable: bool = True) -> None:
255
+ def setStore(self, fieldName: str, enable: bool = True) -> None:
234
256
  """
235
257
  Enable/Disable storing a field (insert into database)
236
258
  """
237
- if field_name in self.__dataclass_fields__:
238
- currentMetadata = self.__dataclass_fields__[field_name].metadata
259
+ if fieldName in self.__dataclass_fields__:
260
+ currentMetadata = cast(
261
+ MetadataDict,
262
+ dict(self.__dataclass_fields__[fieldName].metadata),
263
+ )
239
264
  currentMetadata["store"] = enable
240
- self.__dataclass_fields__[field_name].metadata = currentMetadata
265
+ self.__dataclass_fields__[fieldName].metadata = currentMetadata
241
266
 
242
- def setUpdate(self, field_name: str, enable: bool = True) -> None:
267
+ def setUpdate(self, fieldName: str, enable: bool = True) -> None:
243
268
  """
244
269
  Enable/Disable updating a field (update in database)
245
270
  """
246
- if field_name in self.__dataclass_fields__:
247
- currentMetadata = self.__dataclass_fields__[field_name].metadata
271
+ if fieldName in self.__dataclass_fields__:
272
+ currentMetadata = cast(
273
+ MetadataDict,
274
+ dict(self.__dataclass_fields__[fieldName].metadata),
275
+ )
248
276
  currentMetadata["update"] = enable
249
- self.__dataclass_fields__[field_name].metadata = currentMetadata
277
+ self.__dataclass_fields__[fieldName].metadata = currentMetadata
250
278
 
251
- def setExclude(self, field_name: str, enable: bool = True) -> None:
279
+ def setExclude(self, fieldName: str, enable: bool = True) -> None:
252
280
  """
253
281
  Exclude a field from dict representation
254
282
  """
255
- if field_name in self.__dataclass_fields__:
256
- currentMetadata = dict(self.__dataclass_fields__[field_name].metadata)
283
+ if fieldName in self.__dataclass_fields__:
284
+ currentMetadata = cast(
285
+ MetadataDict,
286
+ dict(self.__dataclass_fields__[fieldName].metadata),
287
+ )
257
288
  currentMetadata["exclude"] = enable
258
- self.__dataclass_fields__[field_name].metadata = currentMetadata
289
+ self.__dataclass_fields__[fieldName].metadata = currentMetadata
259
290
 
260
291
  ########################
261
292
  ### Database methods ###
@@ -272,13 +303,23 @@ class DBDataModel:
272
303
  Store data to database
273
304
  """
274
305
  storeData: dict[str, Any] = {}
275
- for field_name, field_obj in self.__dataclass_fields__.items():
276
- metadata = field_obj.metadata
306
+ for fieldName, fieldObj in self.__dataclass_fields__.items():
307
+ metadata = cast(MetadataDict, fieldObj.metadata)
277
308
  if "store" in metadata and metadata["store"] == True:
278
- storeData[field_name] = getattr(self, field_name)
279
-
280
- if "decode" in metadata and metadata["decode"] is not None:
281
- storeData[field_name] = metadata["decode"](storeData[field_name])
309
+ storeData[fieldName] = getattr(self, fieldName)
310
+
311
+ # If serialize is set, and serialize is a SerializeType,
312
+ # we use our serialization function.
313
+ # Otherwise, we use the provided serialize function
314
+ # and we assume that it is callable
315
+ serialize = metadata.get("serialize", None)
316
+ if serialize is not None:
317
+ if isinstance(serialize, SerializeType):
318
+ storeData[fieldName] = serializeValue(
319
+ storeData[fieldName], serialize
320
+ )
321
+ else:
322
+ storeData[fieldName] = serialize(storeData[fieldName])
282
323
 
283
324
  return storeData
284
325
 
@@ -288,13 +329,23 @@ class DBDataModel:
288
329
  """
289
330
 
290
331
  updateData: dict[str, Any] = {}
291
- for field_name, field_obj in self.__dataclass_fields__.items():
292
- metadata = field_obj.metadata
332
+ for fieldName, fieldObj in self.__dataclass_fields__.items():
333
+ metadata = cast(MetadataDict, fieldObj.metadata)
293
334
  if "update" in metadata and metadata["update"] == True:
294
- updateData[field_name] = getattr(self, field_name)
295
-
296
- if "decode" in metadata and metadata["decode"] is not None:
297
- updateData[field_name] = metadata["decode"](updateData[field_name])
335
+ updateData[fieldName] = getattr(self, fieldName)
336
+
337
+ # If serialize is set, and serialize is a SerializeType,
338
+ # we use our serialization function.
339
+ # Otherwise, we use the provided serialize function
340
+ # and we assume that it is callable
341
+ serialize = metadata.get("serialize", None)
342
+ if serialize is not None:
343
+ if isinstance(serialize, SerializeType):
344
+ updateData[fieldName] = serializeValue(
345
+ updateData[fieldName], serialize
346
+ )
347
+ else:
348
+ updateData[fieldName] = serialize(updateData[fieldName])
298
349
 
299
350
  return updateData
300
351
 
@@ -321,8 +372,7 @@ class DBDefaultsDataModel(DBDataModel):
321
372
  "db_field": ("created_at", "timestamptz"),
322
373
  "store": True,
323
374
  "update": False,
324
- "encode": lambda value: DBDataModel.strToDatetime(value), # type: ignore
325
- "decode": lambda x: x.isoformat(), # type: ignore
375
+ "serialize": SerializeType.DATETIME,
326
376
  },
327
377
  )
328
378
  """created_at is readonly by default and should be present in all tables"""
@@ -333,8 +383,7 @@ class DBDefaultsDataModel(DBDataModel):
333
383
  "db_field": ("updated_at", "timestamptz"),
334
384
  "store": True,
335
385
  "update": True,
336
- "encode": lambda value: DBDataModel.strToDatetime(value), # type: ignore
337
- "decode": lambda x: x.isoformat(), # type: ignore
386
+ "serialize": SerializeType.DATETIME,
338
387
  },
339
388
  )
340
389
  """updated_at is readonly by default and should be present in all tables"""