database-wrapper 0.1.43__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": "f2eab4d26eccad9d9c72955724af6bfe69930e0e",
7
- "git_commit_date": "15.11.2024 04:32",
8
- "app_version": "0.1.43",
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
- self.contextConnection = ContextVar(f"pg_connection_{self.name}", default=None)
55
- self.contextAsyncConnection = ContextVar(
56
- f"pg_async_connection_{self.name}", default=None
92
+ self.contextConnection = ContextVar(f"db_connection_{self.name}", default=None)
93
+ self.contextConnectionAsync = ContextVar(
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,15 +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 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)
8
18
 
9
- from decimal import Decimal
10
- from typing import Any
11
19
 
12
- from psycopg import sql
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]
13
28
 
14
29
 
15
30
  @dataclass
@@ -40,12 +55,12 @@ class DBDataModel:
40
55
  - validate(): Validates the instance.
41
56
 
42
57
  To enable storing and updating fields that by default are not stored or updated, use the following methods:
43
- - setStore(field_name: str, enable: bool = True): Enable/Disable storing a field.
44
- - 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.
45
60
 
46
61
  To exclude a field from the dictionary representation of the instance, set metadata key "exclude" to True.
47
62
  To change exclude status of a field, use the following method:
48
- - 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.
49
64
  """
50
65
 
51
66
  ######################
@@ -99,7 +114,7 @@ class DBDataModel:
99
114
  ### Conversion methods ###
100
115
  ##########################
101
116
 
102
- def fillDataFromDict(self, kwargs: dict[str, Any]):
117
+ def fillDataFromDict(self, kwargs: dict[str, Any]) -> None:
103
118
  fieldNames = set([f.name for f in dataclasses.fields(self)])
104
119
  for key in kwargs:
105
120
  if key in fieldNames:
@@ -108,12 +123,25 @@ class DBDataModel:
108
123
  self.__post_init__()
109
124
 
110
125
  # Init data
111
- def __post_init__(self):
112
- for field_name, field_obj in self.__dataclass_fields__.items():
113
- metadata = field_obj.metadata
114
- encode = metadata.get("encode", None)
115
- if encode is not None:
116
- 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)
117
145
 
118
146
  # String - representation
119
147
  def __repr__(self) -> str:
@@ -128,7 +156,7 @@ class DBDataModel:
128
156
  for field in pairs:
129
157
  classField = self.__dataclass_fields__.get(field[0], None)
130
158
  if classField is not None:
131
- metadata = classField.metadata
159
+ metadata = cast(MetadataDict, classField.metadata)
132
160
  if not "exclude" in metadata or not metadata["exclude"]:
133
161
  newDict[field[0]] = field[1]
134
162
 
@@ -148,32 +176,20 @@ class DBDataModel:
148
176
  "id": {"type": "number"},
149
177
  },
150
178
  }
151
- for field_name, field_obj in self.__dataclass_fields__.items():
152
- metadata = field_obj.metadata
179
+ for fieldName, fieldObj in self.__dataclass_fields__.items():
180
+ metadata = cast(MetadataDict, fieldObj.metadata)
153
181
  assert (
154
182
  "db_field" in metadata
155
183
  and isinstance(metadata["db_field"], tuple)
156
184
  and len(metadata["db_field"]) == 2
157
- ), f"db_field metadata is not set for {field_name}"
185
+ ), f"db_field metadata is not set for {fieldName}"
158
186
  fieldType: str = metadata["db_field"][1]
159
- schema["properties"][field_name] = {"type": fieldType}
187
+ schema["properties"][fieldName] = {"type": fieldType}
160
188
 
161
189
  return schema
162
190
 
163
191
  def jsonEncoder(self, obj: Any) -> Any:
164
- if isinstance(obj, Decimal):
165
- return float(obj)
166
-
167
- if isinstance(obj, datetime.date) or isinstance(obj, datetime.datetime):
168
- return obj.strftime("%Y-%m-%dT%H:%M:%S")
169
-
170
- if isinstance(obj, Enum):
171
- return obj.value
172
-
173
- if isinstance(obj, int) or isinstance(obj, float) or isinstance(obj, str):
174
- return obj
175
-
176
- return str(obj)
192
+ return jsonEncoder(obj)
177
193
 
178
194
  def toJsonString(self, pretty: bool = False) -> str:
179
195
  if pretty:
@@ -230,41 +246,53 @@ class DBDataModel:
230
246
 
231
247
  return 0
232
248
 
233
- def validate(self) -> bool:
249
+ def validate(self) -> Literal[True] | str:
250
+ """
251
+ True if the instance is valid, otherwise an error message.
252
+ """
234
253
  raise NotImplementedError("`validate` is not implemented")
235
254
 
236
- def setStore(self, field_name: str, enable: bool = True) -> None:
255
+ def setStore(self, fieldName: str, enable: bool = True) -> None:
237
256
  """
238
257
  Enable/Disable storing a field (insert into database)
239
258
  """
240
- if field_name in self.__dataclass_fields__:
241
- 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
+ )
242
264
  currentMetadata["store"] = enable
243
- self.__dataclass_fields__[field_name].metadata = currentMetadata
265
+ self.__dataclass_fields__[fieldName].metadata = currentMetadata
244
266
 
245
- def setUpdate(self, field_name: str, enable: bool = True) -> None:
267
+ def setUpdate(self, fieldName: str, enable: bool = True) -> None:
246
268
  """
247
269
  Enable/Disable updating a field (update in database)
248
270
  """
249
- if field_name in self.__dataclass_fields__:
250
- 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
+ )
251
276
  currentMetadata["update"] = enable
252
- self.__dataclass_fields__[field_name].metadata = currentMetadata
277
+ self.__dataclass_fields__[fieldName].metadata = currentMetadata
253
278
 
254
- def setExclude(self, field_name: str, enable: bool = True) -> None:
279
+ def setExclude(self, fieldName: str, enable: bool = True) -> None:
255
280
  """
256
281
  Exclude a field from dict representation
257
282
  """
258
- if field_name in self.__dataclass_fields__:
259
- 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
+ )
260
288
  currentMetadata["exclude"] = enable
261
- self.__dataclass_fields__[field_name].metadata = currentMetadata
289
+ self.__dataclass_fields__[fieldName].metadata = currentMetadata
262
290
 
263
291
  ########################
264
292
  ### Database methods ###
265
293
  ########################
266
294
 
267
- def queryBase(self) -> sql.SQL | sql.Composed | str | None:
295
+ def queryBase(self) -> Any:
268
296
  """
269
297
  Base query for all queries
270
298
  """
@@ -275,13 +303,23 @@ class DBDataModel:
275
303
  Store data to database
276
304
  """
277
305
  storeData: dict[str, Any] = {}
278
- for field_name, field_obj in self.__dataclass_fields__.items():
279
- metadata = field_obj.metadata
306
+ for fieldName, fieldObj in self.__dataclass_fields__.items():
307
+ metadata = cast(MetadataDict, fieldObj.metadata)
280
308
  if "store" in metadata and metadata["store"] == True:
281
- storeData[field_name] = getattr(self, field_name)
282
-
283
- if "decode" in metadata and metadata["decode"] is not None:
284
- 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])
285
323
 
286
324
  return storeData
287
325
 
@@ -291,13 +329,23 @@ class DBDataModel:
291
329
  """
292
330
 
293
331
  updateData: dict[str, Any] = {}
294
- for field_name, field_obj in self.__dataclass_fields__.items():
295
- metadata = field_obj.metadata
332
+ for fieldName, fieldObj in self.__dataclass_fields__.items():
333
+ metadata = cast(MetadataDict, fieldObj.metadata)
296
334
  if "update" in metadata and metadata["update"] == True:
297
- updateData[field_name] = getattr(self, field_name)
298
-
299
- if "decode" in metadata and metadata["decode"] is not None:
300
- 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])
301
349
 
302
350
  return updateData
303
351
 
@@ -324,8 +372,7 @@ class DBDefaultsDataModel(DBDataModel):
324
372
  "db_field": ("created_at", "timestamptz"),
325
373
  "store": True,
326
374
  "update": False,
327
- "encode": lambda value: DBDataModel.strToDatetime(value), # type: ignore
328
- "decode": lambda x: x.isoformat(), # type: ignore
375
+ "serialize": SerializeType.DATETIME,
329
376
  },
330
377
  )
331
378
  """created_at is readonly by default and should be present in all tables"""
@@ -336,8 +383,7 @@ class DBDefaultsDataModel(DBDataModel):
336
383
  "db_field": ("updated_at", "timestamptz"),
337
384
  "store": True,
338
385
  "update": True,
339
- "encode": lambda value: DBDataModel.strToDatetime(value), # type: ignore
340
- "decode": lambda x: x.isoformat(), # type: ignore
386
+ "serialize": SerializeType.DATETIME,
341
387
  },
342
388
  )
343
389
  """updated_at is readonly by default and should be present in all tables"""