database-wrapper 0.2.7__tar.gz → 0.2.9__tar.gz
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-0.2.7 → database_wrapper-0.2.9}/PKG-INFO +6 -6
- {database_wrapper-0.2.7 → database_wrapper-0.2.9}/database_wrapper/__init__.py +3 -0
- {database_wrapper-0.2.7 → database_wrapper-0.2.9}/database_wrapper/config.py +3 -3
- {database_wrapper-0.2.7 → database_wrapper-0.2.9}/database_wrapper/db_data_model.py +23 -9
- database_wrapper-0.2.9/database_wrapper/db_introspector.py +397 -0
- {database_wrapper-0.2.7 → database_wrapper-0.2.9}/database_wrapper/serialization.py +26 -0
- {database_wrapper-0.2.7 → database_wrapper-0.2.9}/database_wrapper.egg-info/PKG-INFO +6 -6
- {database_wrapper-0.2.7 → database_wrapper-0.2.9}/database_wrapper.egg-info/SOURCES.txt +1 -0
- {database_wrapper-0.2.7 → database_wrapper-0.2.9}/database_wrapper.egg-info/requires.txt +5 -5
- {database_wrapper-0.2.7 → database_wrapper-0.2.9}/pyproject.toml +6 -6
- {database_wrapper-0.2.7 → database_wrapper-0.2.9}/README.md +0 -0
- {database_wrapper-0.2.7 → database_wrapper-0.2.9}/database_wrapper/abc.py +0 -0
- {database_wrapper-0.2.7 → database_wrapper-0.2.9}/database_wrapper/common.py +0 -0
- {database_wrapper-0.2.7 → database_wrapper-0.2.9}/database_wrapper/db_backend.py +0 -0
- {database_wrapper-0.2.7 → database_wrapper-0.2.9}/database_wrapper/db_wrapper.py +0 -0
- {database_wrapper-0.2.7 → database_wrapper-0.2.9}/database_wrapper/db_wrapper_async.py +0 -0
- {database_wrapper-0.2.7 → database_wrapper-0.2.9}/database_wrapper/db_wrapper_mixin.py +0 -0
- {database_wrapper-0.2.7 → database_wrapper-0.2.9}/database_wrapper/py.typed +0 -0
- {database_wrapper-0.2.7 → database_wrapper-0.2.9}/database_wrapper/utils/__init__.py +0 -0
- {database_wrapper-0.2.7 → database_wrapper-0.2.9}/database_wrapper/utils/dataclass_addons.py +0 -0
- {database_wrapper-0.2.7 → database_wrapper-0.2.9}/database_wrapper.egg-info/dependency_links.txt +0 -0
- {database_wrapper-0.2.7 → database_wrapper-0.2.9}/database_wrapper.egg-info/top_level.txt +0 -0
- {database_wrapper-0.2.7 → database_wrapper-0.2.9}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: database_wrapper
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
4
4
|
Summary: A Different Approach to Database Wrappers in Python
|
|
5
5
|
Author-email: Gints Murans <gm@gm.lv>
|
|
6
6
|
License: GNU General Public License v3.0 (GPL-3.0)
|
|
@@ -33,15 +33,15 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
33
33
|
Requires-Python: >=3.8
|
|
34
34
|
Description-Content-Type: text/markdown
|
|
35
35
|
Provides-Extra: pgsql
|
|
36
|
-
Requires-Dist: database_wrapper_pgsql==0.2.
|
|
36
|
+
Requires-Dist: database_wrapper_pgsql==0.2.9; extra == "pgsql"
|
|
37
37
|
Provides-Extra: mysql
|
|
38
|
-
Requires-Dist: database_wrapper_mysql==0.2.
|
|
38
|
+
Requires-Dist: database_wrapper_mysql==0.2.9; extra == "mysql"
|
|
39
39
|
Provides-Extra: mssql
|
|
40
|
-
Requires-Dist: database_wrapper_mssql==0.2.
|
|
40
|
+
Requires-Dist: database_wrapper_mssql==0.2.9; extra == "mssql"
|
|
41
41
|
Provides-Extra: sqlite
|
|
42
|
-
Requires-Dist: database_wrapper_sqlite==0.2.
|
|
42
|
+
Requires-Dist: database_wrapper_sqlite==0.2.9; extra == "sqlite"
|
|
43
43
|
Provides-Extra: redis
|
|
44
|
-
Requires-Dist: database_wrapper_redis==0.2.
|
|
44
|
+
Requires-Dist: database_wrapper_redis==0.2.9; extra == "redis"
|
|
45
45
|
Provides-Extra: all
|
|
46
46
|
Requires-Dist: database_wrapper[mssql,mysql,pgsql,redis,sqlite]; extra == "all"
|
|
47
47
|
Provides-Extra: dev
|
|
@@ -15,6 +15,7 @@ from .db_wrapper import DBWrapper
|
|
|
15
15
|
from .db_wrapper_async import DBWrapperAsync
|
|
16
16
|
from .serialization import SerializeType
|
|
17
17
|
from .utils.dataclass_addons import ignore_unknown_kwargs
|
|
18
|
+
from .db_introspector import ColumnMetaIntrospector, DBIntrospector
|
|
18
19
|
|
|
19
20
|
# Set the logger to a quiet default, can be enabled if needed
|
|
20
21
|
logger = logging.getLogger("database_wrapper")
|
|
@@ -40,6 +41,8 @@ __all__ = [
|
|
|
40
41
|
"utils",
|
|
41
42
|
"SerializeType",
|
|
42
43
|
"ignore_unknown_kwargs",
|
|
44
|
+
"ColumnMetaIntrospector",
|
|
45
|
+
"DBIntrospector",
|
|
43
46
|
# Abstract classes
|
|
44
47
|
"ConnectionABC",
|
|
45
48
|
"CursorABC",
|
|
@@ -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.2.
|
|
6
|
+
"git_commit_hash": "aba940d794c0b9d2390f87bd32a86c13769311d7",
|
|
7
|
+
"git_commit_date": "10.10.2025 01:49",
|
|
8
|
+
"app_version": "0.2.9",
|
|
9
9
|
}
|
|
@@ -185,9 +185,7 @@ class DBDataModel:
|
|
|
185
185
|
for field_name, field_obj in self.__dataclass_fields__.items():
|
|
186
186
|
metadata = cast(MetadataDict, field_obj.metadata)
|
|
187
187
|
assert (
|
|
188
|
-
"db_field" in metadata
|
|
189
|
-
and isinstance(metadata["db_field"], tuple)
|
|
190
|
-
and len(metadata["db_field"]) == 2
|
|
188
|
+
"db_field" in metadata and isinstance(metadata["db_field"], tuple) and len(metadata["db_field"]) == 2
|
|
191
189
|
), f"db_field metadata is not set for {field_name}"
|
|
192
190
|
field_type: str = metadata["db_field"][1]
|
|
193
191
|
schema["properties"][field_name] = {"type": field_type}
|
|
@@ -321,9 +319,7 @@ class DBDataModel:
|
|
|
321
319
|
serialize = metadata.get("serialize", None)
|
|
322
320
|
if serialize is not None:
|
|
323
321
|
if isinstance(serialize, SerializeType):
|
|
324
|
-
store_data[field_name] = serialize_value(
|
|
325
|
-
store_data[field_name], serialize
|
|
326
|
-
)
|
|
322
|
+
store_data[field_name] = serialize_value(store_data[field_name], serialize)
|
|
327
323
|
else:
|
|
328
324
|
store_data[field_name] = serialize(store_data[field_name])
|
|
329
325
|
|
|
@@ -347,9 +343,7 @@ class DBDataModel:
|
|
|
347
343
|
serialize = metadata.get("serialize", None)
|
|
348
344
|
if serialize is not None:
|
|
349
345
|
if isinstance(serialize, SerializeType):
|
|
350
|
-
update_data[field_name] = serialize_value(
|
|
351
|
-
update_data[field_name], serialize
|
|
352
|
-
)
|
|
346
|
+
update_data[field_name] = serialize_value(update_data[field_name], serialize)
|
|
353
347
|
else:
|
|
354
348
|
update_data[field_name] = serialize(update_data[field_name])
|
|
355
349
|
|
|
@@ -394,6 +388,26 @@ class DBDefaultsDataModel(DBDataModel):
|
|
|
394
388
|
)
|
|
395
389
|
"""updated_at should be present in all tables and is updated automatically"""
|
|
396
390
|
|
|
391
|
+
disabled_at: datetime.datetime = field(
|
|
392
|
+
default_factory=datetime.datetime.now,
|
|
393
|
+
metadata={
|
|
394
|
+
"db_field": ("disabled_at", "timestamptz"),
|
|
395
|
+
"store": False,
|
|
396
|
+
"update": False,
|
|
397
|
+
"serialize": SerializeType.DATETIME,
|
|
398
|
+
},
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
deleted_at: datetime.datetime = field(
|
|
402
|
+
default_factory=datetime.datetime.now,
|
|
403
|
+
metadata={
|
|
404
|
+
"db_field": ("deleted_at", "timestamptz"),
|
|
405
|
+
"store": False,
|
|
406
|
+
"update": False,
|
|
407
|
+
"serialize": SerializeType.DATETIME,
|
|
408
|
+
},
|
|
409
|
+
)
|
|
410
|
+
|
|
397
411
|
enabled: bool = field(
|
|
398
412
|
default=True,
|
|
399
413
|
metadata={
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
from dataclasses import MISSING, dataclass, make_dataclass, field, fields as dc_fields
|
|
2
|
+
from datetime import datetime, date
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Optional, Type, Union, get_origin, get_args
|
|
6
|
+
|
|
7
|
+
from database_wrapper import DBDefaultsDataModel, MetadataDict, SerializeType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def type_to_str(t: Any) -> str:
|
|
11
|
+
"""Render annotations like 'str | None' or 'RV4ProductionJobStatus | None'."""
|
|
12
|
+
origin = get_origin(t)
|
|
13
|
+
if origin is Union:
|
|
14
|
+
args = list(get_args(t))
|
|
15
|
+
# Optional[T] -> Union[T, NoneType]
|
|
16
|
+
if type(None) in args and len(args) == 2:
|
|
17
|
+
other = args[0] if args[1] is type(None) else args[1]
|
|
18
|
+
return f"{type_to_str(other)} | None"
|
|
19
|
+
# General Union
|
|
20
|
+
return " | ".join(type_to_str(a) for a in args)
|
|
21
|
+
if hasattr(t, "__name__"):
|
|
22
|
+
return t.__name__
|
|
23
|
+
if getattr(t, "__module__", None) and getattr(t, "__qualname__", None):
|
|
24
|
+
return f"{t.__qualname__}"
|
|
25
|
+
return str(t)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _make_enum(name: str, labels: list[str]) -> Enum:
|
|
29
|
+
# Normalize labels to valid identifiers
|
|
30
|
+
members = {}
|
|
31
|
+
for raw in labels:
|
|
32
|
+
key = raw.upper().replace(" ", "_")
|
|
33
|
+
# avoid starting with digit
|
|
34
|
+
if key and key[0].isdigit():
|
|
35
|
+
key = f"_{key}"
|
|
36
|
+
members[key] = raw
|
|
37
|
+
return Enum(name, members)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class ColumnMetaIntrospector:
|
|
42
|
+
col_name: str
|
|
43
|
+
db_type: str
|
|
44
|
+
is_nullable: bool
|
|
45
|
+
has_default: bool
|
|
46
|
+
default_expr: str | None = None
|
|
47
|
+
enum_labels: list[str] | None = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class DBIntrospector:
|
|
51
|
+
conn: Any
|
|
52
|
+
|
|
53
|
+
def __init__(self, dbCursor: Any = None):
|
|
54
|
+
self.conn = dbCursor
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def _default_class_name(schema: str, table: str) -> str:
|
|
58
|
+
# Very naive PascalCase maker
|
|
59
|
+
def pascal(s: str) -> str:
|
|
60
|
+
return "".join(p.capitalize() for p in s.replace("-", "_").split("_"))
|
|
61
|
+
|
|
62
|
+
return f"{pascal(schema)}{pascal(table)}"
|
|
63
|
+
|
|
64
|
+
def get_table_columns(self, schema: str, table: str) -> list[ColumnMetaIntrospector]:
|
|
65
|
+
raise NotImplementedError
|
|
66
|
+
|
|
67
|
+
def map_db_type(self, db_type: str) -> str:
|
|
68
|
+
raise NotImplementedError
|
|
69
|
+
|
|
70
|
+
def get_schema_table_name(self, full_table: str) -> tuple[str, str]:
|
|
71
|
+
(schema, table) = full_table.split(".") if "." in full_table else ("public", full_table)
|
|
72
|
+
return (schema, table)
|
|
73
|
+
|
|
74
|
+
def is_meta_field(self, col_name: str) -> bool:
|
|
75
|
+
"""
|
|
76
|
+
Return True if the column is a common meta field we want to skip.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
col_name: The name of the column
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
col_name == "id"
|
|
84
|
+
or col_name == "created_at"
|
|
85
|
+
or col_name == "updated_at"
|
|
86
|
+
or col_name == "disabled_at"
|
|
87
|
+
or col_name == "deleted_at"
|
|
88
|
+
or col_name == "enabled"
|
|
89
|
+
or col_name == "deleted"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def generate_dataclass(
|
|
93
|
+
self,
|
|
94
|
+
table_name: str,
|
|
95
|
+
*,
|
|
96
|
+
class_name: Optional[str] = None,
|
|
97
|
+
base: Type[DBDefaultsDataModel] = DBDefaultsDataModel,
|
|
98
|
+
enum_overrides: dict[str, Type[Enum]] | None = None,
|
|
99
|
+
defaults_for_nullable: bool = True,
|
|
100
|
+
include_id_field: bool = True,
|
|
101
|
+
) -> Type[DBDefaultsDataModel]:
|
|
102
|
+
(schema, table) = self.get_schema_table_name(table_name)
|
|
103
|
+
cols = self.get_table_columns(schema, table)
|
|
104
|
+
if not cols:
|
|
105
|
+
raise ValueError(f"No columns found for {schema}.{table}")
|
|
106
|
+
|
|
107
|
+
class_name = class_name or self._default_class_name(schema, table)
|
|
108
|
+
enum_overrides = enum_overrides or {}
|
|
109
|
+
|
|
110
|
+
fields_defs = []
|
|
111
|
+
|
|
112
|
+
for c in cols:
|
|
113
|
+
# Skip meta fields
|
|
114
|
+
if self.is_meta_field(c.col_name):
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
# Enums
|
|
118
|
+
enum_class: Optional[Type[Enum]] = None
|
|
119
|
+
if c.enum_labels:
|
|
120
|
+
enum_class = enum_overrides.get(c.col_name)
|
|
121
|
+
if not enum_class:
|
|
122
|
+
enum_class = _make_enum(f"{class_name}_{c.col_name}_Enum", list(c.enum_labels))
|
|
123
|
+
py_type = enum_class
|
|
124
|
+
serialize = SerializeType.ENUM
|
|
125
|
+
else:
|
|
126
|
+
py_type, serialize = self.map_db_type(c.db_type)
|
|
127
|
+
|
|
128
|
+
# Optional typing if nullable
|
|
129
|
+
if c.is_nullable:
|
|
130
|
+
ann = Optional[py_type] # type: ignore
|
|
131
|
+
else:
|
|
132
|
+
ann = py_type # type: ignore
|
|
133
|
+
|
|
134
|
+
# Default value choice
|
|
135
|
+
default = None
|
|
136
|
+
default_factory = None
|
|
137
|
+
if c.is_nullable:
|
|
138
|
+
default = None
|
|
139
|
+
else:
|
|
140
|
+
# give some sane defaults for common not-nullables that aren't id/serial
|
|
141
|
+
if py_type is bool:
|
|
142
|
+
default = False
|
|
143
|
+
elif py_type in (int, float, str):
|
|
144
|
+
default = py_type() # 0, 0.0, ""
|
|
145
|
+
elif py_type is datetime:
|
|
146
|
+
default_factory = datetime.now
|
|
147
|
+
elif py_type is datetime.date:
|
|
148
|
+
default_factory = date.today
|
|
149
|
+
elif enum_class:
|
|
150
|
+
# pick first enum value as default
|
|
151
|
+
enum_values = list(enum_class)
|
|
152
|
+
if enum_values:
|
|
153
|
+
default = enum_values[0]
|
|
154
|
+
elif py_type is dict:
|
|
155
|
+
default_factory = dict
|
|
156
|
+
elif py_type is list:
|
|
157
|
+
default_factory = list
|
|
158
|
+
elif py_type is set:
|
|
159
|
+
default_factory = set
|
|
160
|
+
elif py_type is bytes:
|
|
161
|
+
default = bytes()
|
|
162
|
+
else:
|
|
163
|
+
# Leave unset so dataclass enforces passing it explicitly
|
|
164
|
+
default = None if defaults_for_nullable else None
|
|
165
|
+
|
|
166
|
+
md: MetadataDict = {
|
|
167
|
+
"db_field": (c.col_name, c.db_type),
|
|
168
|
+
"store": True, # opinion: new rows insert everything unless you override
|
|
169
|
+
"update": True, # opinion: updates allowed unless you override
|
|
170
|
+
}
|
|
171
|
+
if serialize:
|
|
172
|
+
md["serialize"] = serialize
|
|
173
|
+
if enum_class:
|
|
174
|
+
md["enum_class"] = enum_class
|
|
175
|
+
|
|
176
|
+
if default_factory:
|
|
177
|
+
fld = field(default_factory=default_factory, metadata=md)
|
|
178
|
+
else:
|
|
179
|
+
fld = field(default=default, metadata=md)
|
|
180
|
+
fields_defs.append(
|
|
181
|
+
(
|
|
182
|
+
c.col_name,
|
|
183
|
+
ann,
|
|
184
|
+
fld,
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Build class with properties schemaName/tableName
|
|
189
|
+
# We’ll generate methods dynamically and attach.
|
|
190
|
+
cls = make_dataclass(
|
|
191
|
+
class_name,
|
|
192
|
+
fields_defs,
|
|
193
|
+
bases=(base,),
|
|
194
|
+
namespace={}, # we’ll add properties below
|
|
195
|
+
frozen=False,
|
|
196
|
+
eq=True,
|
|
197
|
+
repr=True,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Attach schemaName/tableName as properties
|
|
201
|
+
def _schemaName(self) -> str:
|
|
202
|
+
return schema
|
|
203
|
+
|
|
204
|
+
def _tableName(self) -> str:
|
|
205
|
+
return table
|
|
206
|
+
|
|
207
|
+
setattr(cls, "schemaName", property(_schemaName))
|
|
208
|
+
setattr(cls, "tableName", property(_tableName))
|
|
209
|
+
|
|
210
|
+
return cls
|
|
211
|
+
|
|
212
|
+
# TODO: Need to improve handling of imports for external classes, including enums.
|
|
213
|
+
def render_dataclass_source(
|
|
214
|
+
self,
|
|
215
|
+
cls: Type,
|
|
216
|
+
table_name: str,
|
|
217
|
+
*,
|
|
218
|
+
extra_imports: list[str] | None = None,
|
|
219
|
+
emit_ignore_unknown_kwargs: bool = True,
|
|
220
|
+
) -> str:
|
|
221
|
+
"""
|
|
222
|
+
Turn a runtime dataclass into a source file close to user's example.
|
|
223
|
+
|
|
224
|
+
- Hardcodes schemaName/tableName.
|
|
225
|
+
- Emits @ignore_unknown_kwargs() above @dataclass (optional).
|
|
226
|
+
- Renders fields as:
|
|
227
|
+
name: T | None = field(
|
|
228
|
+
default=..., or default_factory=...,
|
|
229
|
+
metadata=MetadataDict(
|
|
230
|
+
db_field=("col", "pg_type"),
|
|
231
|
+
store=True/False,
|
|
232
|
+
update=True/False,
|
|
233
|
+
[serialize=SerializeType.X],
|
|
234
|
+
[enum_class=SomeEnum],
|
|
235
|
+
),
|
|
236
|
+
)
|
|
237
|
+
- If an enum_class was dynamically created (module == 'enum'), embeds it into the file.
|
|
238
|
+
Otherwise, adds a "from {module} import {name}" import.
|
|
239
|
+
- Adds a toDict() that enumerates fields and applies .isoformat()
|
|
240
|
+
for fields with serialize=SerializeType.DATETIME.
|
|
241
|
+
"""
|
|
242
|
+
# Collect enums: dynamic vs external
|
|
243
|
+
extra_imports = extra_imports or []
|
|
244
|
+
dynamic_enums: list[tuple[str, list[tuple[str, Any]]]] = []
|
|
245
|
+
external_enum_imports: set[tuple[str, str]] = set()
|
|
246
|
+
for f in dc_fields(cls):
|
|
247
|
+
md: MetadataDict = dict(f.metadata) if f.metadata else {} # type: ignore
|
|
248
|
+
enum_class = md.get("enum_class")
|
|
249
|
+
if enum_class:
|
|
250
|
+
mod = getattr(enum_class, "__module__", "")
|
|
251
|
+
name = getattr(enum_class, "__name__", "UnknownEnum")
|
|
252
|
+
# If it's the built-in Enum module (meaning we made it dynamically),
|
|
253
|
+
# embed it. Otherwise, import from its module.
|
|
254
|
+
if mod == "enum":
|
|
255
|
+
members = [(m.name, m.value) for m in enum_class] # type: ignore
|
|
256
|
+
dynamic_enums.append((name, members))
|
|
257
|
+
else:
|
|
258
|
+
external_enum_imports.add((mod, name))
|
|
259
|
+
|
|
260
|
+
lines: list[str] = []
|
|
261
|
+
# Imports
|
|
262
|
+
lines.append("from typing import Any, Optional")
|
|
263
|
+
lines.append("from datetime import datetime")
|
|
264
|
+
lines.append("from dataclasses import dataclass, field")
|
|
265
|
+
lines.append("")
|
|
266
|
+
lines.append("from database_wrapper import MetadataDict, DBDefaultsDataModel, SerializeType")
|
|
267
|
+
if emit_ignore_unknown_kwargs:
|
|
268
|
+
lines.append("from database_wrapper.utils import ignore_unknown_kwargs")
|
|
269
|
+
if dynamic_enums:
|
|
270
|
+
lines.append("from enum import Enum")
|
|
271
|
+
for mod, name in sorted(external_enum_imports):
|
|
272
|
+
lines.append(f"from {mod} import {name}")
|
|
273
|
+
for imp in extra_imports:
|
|
274
|
+
lines.append(imp)
|
|
275
|
+
lines.append("")
|
|
276
|
+
|
|
277
|
+
# Dynamic enums embedded
|
|
278
|
+
for name, members in dynamic_enums:
|
|
279
|
+
lines.append(f"class {name}(Enum):")
|
|
280
|
+
for k, v in members:
|
|
281
|
+
lines.append(f" {k} = {repr(v)}")
|
|
282
|
+
lines.append("")
|
|
283
|
+
|
|
284
|
+
# Class header
|
|
285
|
+
(schema, table) = self.get_schema_table_name(table_name)
|
|
286
|
+
if emit_ignore_unknown_kwargs:
|
|
287
|
+
lines.append("@ignore_unknown_kwargs()")
|
|
288
|
+
lines.append("@dataclass")
|
|
289
|
+
lines.append(f"class {cls.__name__}(DBDefaultsDataModel):")
|
|
290
|
+
lines.append(' """Auto-generated from database schema"""')
|
|
291
|
+
lines.append("")
|
|
292
|
+
lines.append(" @property")
|
|
293
|
+
lines.append(" def schemaName(self) -> str:")
|
|
294
|
+
lines.append(f" return {schema!r}")
|
|
295
|
+
lines.append("")
|
|
296
|
+
lines.append(" @property")
|
|
297
|
+
lines.append(" def tableName(self) -> str:")
|
|
298
|
+
lines.append(f" return {table!r}")
|
|
299
|
+
lines.append("")
|
|
300
|
+
|
|
301
|
+
# Render fields (skip the inherited ones we know exist on base if they aren't present here)
|
|
302
|
+
for f in dc_fields(cls):
|
|
303
|
+
md: MetadataDict = dict(f.metadata) if f.metadata else {} # type: ignore
|
|
304
|
+
# We always render all fields that exist in this dataclass (your make_dataclass created them)
|
|
305
|
+
db_field = md.get("db_field", (f.name, "Any"))
|
|
306
|
+
if not (isinstance(db_field, tuple) and len(db_field) == 2):
|
|
307
|
+
col_name, db_type = f.name, "Any"
|
|
308
|
+
else:
|
|
309
|
+
col_name, db_type = db_field
|
|
310
|
+
|
|
311
|
+
# Skip meta fields
|
|
312
|
+
if self.is_meta_field(col_name):
|
|
313
|
+
continue
|
|
314
|
+
|
|
315
|
+
store = bool(md.get("store", False))
|
|
316
|
+
update = bool(md.get("update", False))
|
|
317
|
+
serialize = md.get("serialize")
|
|
318
|
+
enum_class = md.get("enum_class")
|
|
319
|
+
|
|
320
|
+
# Type annotation
|
|
321
|
+
ann_str = type_to_str(f.type)
|
|
322
|
+
|
|
323
|
+
# Default
|
|
324
|
+
# dataclasses._MISSING_TYPE shows up for no default; we’ll set explicit default=None
|
|
325
|
+
# if annotation is Optional[...] and no concrete default.
|
|
326
|
+
default_val = getattr(f, "default", None)
|
|
327
|
+
default_factory = getattr(f, "default_factory", None)
|
|
328
|
+
|
|
329
|
+
# Build field(...) call
|
|
330
|
+
lines.append(f" {f.name}: {ann_str} = field(")
|
|
331
|
+
|
|
332
|
+
if default_factory is not None and default_factory is not MISSING:
|
|
333
|
+
lines.append(f" default_factory={getattr(default_factory, '__qualname__', None)},")
|
|
334
|
+
elif default_val is not None and default_val is not MISSING:
|
|
335
|
+
lines.append(f" default={repr(default_val)},")
|
|
336
|
+
else:
|
|
337
|
+
# be explicit for optionals; otherwise we omit default and let required be required
|
|
338
|
+
if "| None" in ann_str:
|
|
339
|
+
lines.append(" default=None,")
|
|
340
|
+
|
|
341
|
+
# Metadata
|
|
342
|
+
lines.append(" metadata=MetadataDict(")
|
|
343
|
+
lines.append(f" db_field=({col_name!r}, {db_type!r}),")
|
|
344
|
+
lines.append(f" store={str(store)},")
|
|
345
|
+
lines.append(f" update={str(update)},")
|
|
346
|
+
if serialize:
|
|
347
|
+
lines.append(f" serialize=SerializeType.{serialize.name},")
|
|
348
|
+
if enum_class:
|
|
349
|
+
lines.append(f" enum_class={enum_class.__name__},")
|
|
350
|
+
lines.append(" ),")
|
|
351
|
+
lines.append(" )")
|
|
352
|
+
lines.append("")
|
|
353
|
+
|
|
354
|
+
# toDict() that matches your style and ISO-formats DATETIME fields
|
|
355
|
+
# We enumerate in a stable order and treat DATETIME specially.
|
|
356
|
+
# (jsonEncoder in your base also works, but you asked for "close to example".)
|
|
357
|
+
lines.append(" # Methods")
|
|
358
|
+
lines.append(" def toDict(self) -> dict[str, Any]:")
|
|
359
|
+
lines.append(" out: dict[str, Any] = {}")
|
|
360
|
+
lines.append(" # Explicitly list each field (stable order)")
|
|
361
|
+
for f in dc_fields(cls):
|
|
362
|
+
md: MetadataDict = dict(f.metadata) if f.metadata else {} # type: ignore
|
|
363
|
+
serialize = md.get("serialize")
|
|
364
|
+
key = f.name
|
|
365
|
+
if serialize == SerializeType.DATETIME:
|
|
366
|
+
lines.append(f" out[{key!r}] = self.{key}.isoformat() if self.{key} else None")
|
|
367
|
+
else:
|
|
368
|
+
lines.append(f" out[{key!r}] = self.{key}")
|
|
369
|
+
lines.append(" return out")
|
|
370
|
+
|
|
371
|
+
return "\n".join(lines)
|
|
372
|
+
|
|
373
|
+
def save_to_file(self, class_model_source: str, filepath: str, overwrite: bool) -> str:
|
|
374
|
+
"""
|
|
375
|
+
Render `cls` to a Python source file and save it to `filepath`.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
cls: The generated dataclass type to render.
|
|
379
|
+
class_model_source: The source code of the class to write.
|
|
380
|
+
filepath: Path to write the file to.
|
|
381
|
+
overwrite: If False and file exists, raises FileExistsError.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
The absolute path of the written file.
|
|
385
|
+
"""
|
|
386
|
+
path = Path(filepath)
|
|
387
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
388
|
+
|
|
389
|
+
if path.exists() and not overwrite:
|
|
390
|
+
raise FileExistsError(f"Refusing to overwrite existing file: {path}")
|
|
391
|
+
|
|
392
|
+
# Normalize line endings + ensure trailing newline
|
|
393
|
+
if not class_model_source.endswith("\n"):
|
|
394
|
+
class_model_source += "\n"
|
|
395
|
+
|
|
396
|
+
path.write_text(class_model_source, encoding="utf-8", newline="\n")
|
|
397
|
+
return str(path.resolve())
|
|
@@ -9,6 +9,8 @@ from zoneinfo import ZoneInfo
|
|
|
9
9
|
|
|
10
10
|
class SerializeType(Enum):
|
|
11
11
|
DATETIME = "datetime"
|
|
12
|
+
DATE = "date"
|
|
13
|
+
TIME = "time"
|
|
12
14
|
JSON = "json"
|
|
13
15
|
ENUM = "enum"
|
|
14
16
|
|
|
@@ -36,6 +38,18 @@ def serialize_value(value: Any, s_type: SerializeType) -> Any:
|
|
|
36
38
|
|
|
37
39
|
return value.isoformat()
|
|
38
40
|
|
|
41
|
+
if s_type == SerializeType.DATE:
|
|
42
|
+
if not isinstance(value, datetime.date):
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
return value.isoformat()
|
|
46
|
+
|
|
47
|
+
if s_type == SerializeType.TIME:
|
|
48
|
+
if not isinstance(value, datetime.time):
|
|
49
|
+
return value
|
|
50
|
+
|
|
51
|
+
return value.isoformat()
|
|
52
|
+
|
|
39
53
|
if s_type == SerializeType.JSON:
|
|
40
54
|
return json.dumps(value, default=json_encoder)
|
|
41
55
|
|
|
@@ -67,6 +81,18 @@ def deserialize_value(
|
|
|
67
81
|
|
|
68
82
|
return datetime.datetime.fromisoformat(value)
|
|
69
83
|
|
|
84
|
+
if s_type == SerializeType.DATE:
|
|
85
|
+
if isinstance(value, datetime.date):
|
|
86
|
+
return value
|
|
87
|
+
|
|
88
|
+
return datetime.date.fromisoformat(str(value))
|
|
89
|
+
|
|
90
|
+
if s_type == SerializeType.TIME:
|
|
91
|
+
if isinstance(value, datetime.time):
|
|
92
|
+
return value
|
|
93
|
+
|
|
94
|
+
return datetime.time.fromisoformat(str(value))
|
|
95
|
+
|
|
70
96
|
if s_type == SerializeType.JSON:
|
|
71
97
|
if isinstance(value, dict) or isinstance(value, list) or value is None:
|
|
72
98
|
return value # type: ignore
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: database_wrapper
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
4
4
|
Summary: A Different Approach to Database Wrappers in Python
|
|
5
5
|
Author-email: Gints Murans <gm@gm.lv>
|
|
6
6
|
License: GNU General Public License v3.0 (GPL-3.0)
|
|
@@ -33,15 +33,15 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
33
33
|
Requires-Python: >=3.8
|
|
34
34
|
Description-Content-Type: text/markdown
|
|
35
35
|
Provides-Extra: pgsql
|
|
36
|
-
Requires-Dist: database_wrapper_pgsql==0.2.
|
|
36
|
+
Requires-Dist: database_wrapper_pgsql==0.2.9; extra == "pgsql"
|
|
37
37
|
Provides-Extra: mysql
|
|
38
|
-
Requires-Dist: database_wrapper_mysql==0.2.
|
|
38
|
+
Requires-Dist: database_wrapper_mysql==0.2.9; extra == "mysql"
|
|
39
39
|
Provides-Extra: mssql
|
|
40
|
-
Requires-Dist: database_wrapper_mssql==0.2.
|
|
40
|
+
Requires-Dist: database_wrapper_mssql==0.2.9; extra == "mssql"
|
|
41
41
|
Provides-Extra: sqlite
|
|
42
|
-
Requires-Dist: database_wrapper_sqlite==0.2.
|
|
42
|
+
Requires-Dist: database_wrapper_sqlite==0.2.9; extra == "sqlite"
|
|
43
43
|
Provides-Extra: redis
|
|
44
|
-
Requires-Dist: database_wrapper_redis==0.2.
|
|
44
|
+
Requires-Dist: database_wrapper_redis==0.2.9; extra == "redis"
|
|
45
45
|
Provides-Extra: all
|
|
46
46
|
Requires-Dist: database_wrapper[mssql,mysql,pgsql,redis,sqlite]; extra == "all"
|
|
47
47
|
Provides-Extra: dev
|
|
@@ -6,6 +6,7 @@ database_wrapper/common.py
|
|
|
6
6
|
database_wrapper/config.py
|
|
7
7
|
database_wrapper/db_backend.py
|
|
8
8
|
database_wrapper/db_data_model.py
|
|
9
|
+
database_wrapper/db_introspector.py
|
|
9
10
|
database_wrapper/db_wrapper.py
|
|
10
11
|
database_wrapper/db_wrapper_async.py
|
|
11
12
|
database_wrapper/db_wrapper_mixin.py
|
|
@@ -17,16 +17,16 @@ mysqlclient>=2.2.2
|
|
|
17
17
|
pymssql>=2.2.10
|
|
18
18
|
|
|
19
19
|
[mssql]
|
|
20
|
-
database_wrapper_mssql==0.2.
|
|
20
|
+
database_wrapper_mssql==0.2.9
|
|
21
21
|
|
|
22
22
|
[mysql]
|
|
23
|
-
database_wrapper_mysql==0.2.
|
|
23
|
+
database_wrapper_mysql==0.2.9
|
|
24
24
|
|
|
25
25
|
[pgsql]
|
|
26
|
-
database_wrapper_pgsql==0.2.
|
|
26
|
+
database_wrapper_pgsql==0.2.9
|
|
27
27
|
|
|
28
28
|
[redis]
|
|
29
|
-
database_wrapper_redis==0.2.
|
|
29
|
+
database_wrapper_redis==0.2.9
|
|
30
30
|
|
|
31
31
|
[sqlite]
|
|
32
|
-
database_wrapper_sqlite==0.2.
|
|
32
|
+
database_wrapper_sqlite==0.2.9
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "database_wrapper"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.9"
|
|
8
8
|
description = "A Different Approach to Database Wrappers in Python"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -45,11 +45,11 @@ Code = "https://github.com/gintsmurans/py_database_wrapper"
|
|
|
45
45
|
Download = "https://pypi.org/project/database_wrapper/"
|
|
46
46
|
|
|
47
47
|
[project.optional-dependencies]
|
|
48
|
-
pgsql = ["database_wrapper_pgsql == 0.2.
|
|
49
|
-
mysql = ["database_wrapper_mysql == 0.2.
|
|
50
|
-
mssql = ["database_wrapper_mssql == 0.2.
|
|
51
|
-
sqlite = ["database_wrapper_sqlite == 0.2.
|
|
52
|
-
redis = ["database_wrapper_redis == 0.2.
|
|
48
|
+
pgsql = ["database_wrapper_pgsql == 0.2.9"]
|
|
49
|
+
mysql = ["database_wrapper_mysql == 0.2.9"]
|
|
50
|
+
mssql = ["database_wrapper_mssql == 0.2.9"]
|
|
51
|
+
sqlite = ["database_wrapper_sqlite == 0.2.9"]
|
|
52
|
+
redis = ["database_wrapper_redis == 0.2.9"]
|
|
53
53
|
all = ["database_wrapper[pgsql,mysql,mssql,sqlite,redis]"]
|
|
54
54
|
dev = [
|
|
55
55
|
# Development
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{database_wrapper-0.2.7 → database_wrapper-0.2.9}/database_wrapper/utils/dataclass_addons.py
RENAMED
|
File without changes
|
{database_wrapper-0.2.7 → database_wrapper-0.2.9}/database_wrapper.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|