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.
- database_wrapper/__init__.py +12 -1
- database_wrapper/abc.py +68 -0
- database_wrapper/config.py +3 -3
- database_wrapper/db_backend.py +133 -44
- database_wrapper/db_data_model.py +108 -62
- database_wrapper/db_wrapper.py +43 -123
- database_wrapper/db_wrapper_async.py +54 -118
- database_wrapper/db_wrapper_mixin.py +18 -44
- database_wrapper/serialization.py +82 -0
- database_wrapper/utils/__init__.py +0 -2
- database_wrapper/utils/dataclass_addons.py +1 -1
- {database_wrapper-0.1.43.dist-info → database_wrapper-0.1.72.dist-info}/METADATA +22 -20
- database_wrapper-0.1.72.dist-info/RECORD +17 -0
- {database_wrapper-0.1.43.dist-info → database_wrapper-0.1.72.dist-info}/WHEEL +1 -1
- database_wrapper/utils/timer.py +0 -297
- database_wrapper-0.1.43.dist-info/RECORD +0 -16
- {database_wrapper-0.1.43.dist-info → database_wrapper-0.1.72.dist-info}/top_level.txt +0 -0
database_wrapper/__init__.py
CHANGED
|
@@ -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
|
]
|
database_wrapper/abc.py
ADDED
|
@@ -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: ...
|
database_wrapper/config.py
CHANGED
|
@@ -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": "
|
|
7
|
-
"git_commit_date": "
|
|
8
|
-
"app_version": "0.1.
|
|
6
|
+
"git_commit_hash": "943e0234156c4a164061e5af9fb9dbd8c3041f6f",
|
|
7
|
+
"git_commit_date": "26.11.2024 22:09",
|
|
8
|
+
"app_version": "0.1.72",
|
|
9
9
|
}
|
database_wrapper/db_backend.py
CHANGED
|
@@ -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
|
-
|
|
36
|
+
""" Connection used in context manager """
|
|
16
37
|
|
|
17
|
-
|
|
38
|
+
contextConnectionAsync: ContextVar[Any | None]
|
|
39
|
+
""" Connection used in async context manager """
|
|
18
40
|
|
|
19
|
-
|
|
20
|
-
|
|
41
|
+
loggerName: str
|
|
42
|
+
""" Logger name """
|
|
21
43
|
|
|
22
|
-
name: str
|
|
23
44
|
logger: logging.Logger
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
self.
|
|
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"
|
|
55
|
-
self.
|
|
56
|
-
f"
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
132
|
+
##################
|
|
133
|
+
### Connection ###
|
|
134
|
+
##################
|
|
135
|
+
|
|
136
|
+
def openPool(self) -> Any:
|
|
137
|
+
"""Open connection pool"""
|
|
138
|
+
...
|
|
85
139
|
|
|
86
|
-
|
|
140
|
+
def closePool(self) -> Any:
|
|
141
|
+
"""Close connection pool"""
|
|
142
|
+
...
|
|
143
|
+
|
|
144
|
+
def open(self) -> Any:
|
|
87
145
|
"""Connect to database"""
|
|
88
|
-
|
|
146
|
+
...
|
|
89
147
|
|
|
90
|
-
def close(self) ->
|
|
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
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
"""
|
|
125
|
-
Create new connection
|
|
206
|
+
####################
|
|
207
|
+
### Transactions ###
|
|
208
|
+
####################
|
|
126
209
|
|
|
127
|
-
|
|
210
|
+
def beginTransaction(self) -> Any:
|
|
211
|
+
"""Start transaction"""
|
|
212
|
+
raise Exception("Not implemented")
|
|
128
213
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
"""
|
|
214
|
+
def commitTransaction(self) -> Any:
|
|
215
|
+
"""Commit transaction"""
|
|
132
216
|
raise Exception("Not implemented")
|
|
133
217
|
|
|
134
|
-
|
|
135
|
-
"""
|
|
136
|
-
|
|
218
|
+
def rollbackTransaction(self) -> Any:
|
|
219
|
+
"""Rollback transaction"""
|
|
220
|
+
raise Exception("Not implemented")
|
|
137
221
|
|
|
138
|
-
|
|
139
|
-
|
|
222
|
+
# @contextmanager
|
|
223
|
+
def transaction(self, dbConn: Any = None) -> Any:
|
|
224
|
+
"""
|
|
225
|
+
Transaction context manager
|
|
140
226
|
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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) ->
|
|
244
|
+
def commit(self) -> Any:
|
|
156
245
|
"""Commit DB queries"""
|
|
157
246
|
raise Exception("Not implemented")
|
|
158
247
|
|
|
159
|
-
def rollback(self) ->
|
|
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
|
-
|
|
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(
|
|
44
|
-
- setUpdate(
|
|
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(
|
|
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
|
|
113
|
-
metadata =
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
152
|
-
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 {
|
|
185
|
+
), f"db_field metadata is not set for {fieldName}"
|
|
158
186
|
fieldType: str = metadata["db_field"][1]
|
|
159
|
-
schema["properties"][
|
|
187
|
+
schema["properties"][fieldName] = {"type": fieldType}
|
|
160
188
|
|
|
161
189
|
return schema
|
|
162
190
|
|
|
163
191
|
def jsonEncoder(self, obj: Any) -> Any:
|
|
164
|
-
|
|
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) ->
|
|
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,
|
|
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
|
|
241
|
-
currentMetadata =
|
|
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__[
|
|
265
|
+
self.__dataclass_fields__[fieldName].metadata = currentMetadata
|
|
244
266
|
|
|
245
|
-
def setUpdate(self,
|
|
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
|
|
250
|
-
currentMetadata =
|
|
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__[
|
|
277
|
+
self.__dataclass_fields__[fieldName].metadata = currentMetadata
|
|
253
278
|
|
|
254
|
-
def setExclude(self,
|
|
279
|
+
def setExclude(self, fieldName: str, enable: bool = True) -> None:
|
|
255
280
|
"""
|
|
256
281
|
Exclude a field from dict representation
|
|
257
282
|
"""
|
|
258
|
-
if
|
|
259
|
-
currentMetadata =
|
|
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__[
|
|
289
|
+
self.__dataclass_fields__[fieldName].metadata = currentMetadata
|
|
262
290
|
|
|
263
291
|
########################
|
|
264
292
|
### Database methods ###
|
|
265
293
|
########################
|
|
266
294
|
|
|
267
|
-
def queryBase(self) ->
|
|
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
|
|
279
|
-
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[
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
|
295
|
-
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[
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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"""
|