dclassql 0.1.0__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.
- dclassql/__init__.py +12 -0
- dclassql/cli.py +123 -0
- dclassql/codegen.py +508 -0
- dclassql/db_pool.py +76 -0
- dclassql/model_inspector.py +383 -0
- dclassql/push/__init__.py +60 -0
- dclassql/push/base.py +417 -0
- dclassql/push/sqlite.py +195 -0
- dclassql/runtime/backends.py +669 -0
- dclassql/runtime/datasource.py +39 -0
- dclassql/table_spec.py +142 -0
- dclassql/unwarp.py +12 -0
- dclassql-0.1.0.dist-info/METADATA +164 -0
- dclassql-0.1.0.dist-info/RECORD +16 -0
- dclassql-0.1.0.dist-info/WHEEL +4 -0
- dclassql-0.1.0.dist-info/entry_points.txt +3 -0
dclassql/db_pool.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import sqlite3
|
|
3
|
+
import threading
|
|
4
|
+
from typing import Callable, Concatenate, Protocol
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class HasLocalClass(Protocol):
|
|
8
|
+
_local: threading.local
|
|
9
|
+
|
|
10
|
+
def save_local[C: HasLocalClass, **P, T](func: Callable[Concatenate[type[C], P], T]) -> Callable[Concatenate[type[C], P], T]:
|
|
11
|
+
@functools.wraps(func)
|
|
12
|
+
def wrapper(cls: type[C], *args: P.args, **kwargs: P.kwargs) -> T:
|
|
13
|
+
field_name = func.__name__
|
|
14
|
+
if hasattr(cls._local, field_name):
|
|
15
|
+
return getattr(cls._local, field_name)
|
|
16
|
+
|
|
17
|
+
r = func(cls, *args, **kwargs)
|
|
18
|
+
setattr(cls._local, field_name, r)
|
|
19
|
+
return r
|
|
20
|
+
return wrapper
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BaseDBPool:
|
|
24
|
+
''' Thread-level database pool base class. Methods decorated with `@save_local` are cached in `threading.local()`. Usage example:
|
|
25
|
+
```python
|
|
26
|
+
class ExampleDBPool(BaseDBPool):
|
|
27
|
+
sqlite_db_path = 'data/news.db'
|
|
28
|
+
visitor_sqlite_db_path = 'data/visitors.db'
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
@save_local
|
|
32
|
+
def sqlite_conn(cls) -> sqlite3.Connection:
|
|
33
|
+
conn = sqlite3.connect(cls.sqlite_db_path, check_same_thread=False)
|
|
34
|
+
cls._setup_sqlite_db(conn)
|
|
35
|
+
return conn
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
@save_local
|
|
39
|
+
def fastlite_conn(cls):
|
|
40
|
+
from fastlite import database
|
|
41
|
+
fastlite_db = database(cls.sqlite_db_path)
|
|
42
|
+
return fastlite_db
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
@save_local
|
|
46
|
+
def fastlite_conn_visitor(cls):
|
|
47
|
+
from fastlite import database
|
|
48
|
+
fastlite_db_visitor = database(cls.visitor_sqlite_db_path)
|
|
49
|
+
cls._setup_sqlite_db(fastlite_db_visitor.conn)
|
|
50
|
+
return fastlite_db_visitor
|
|
51
|
+
```
|
|
52
|
+
'''
|
|
53
|
+
|
|
54
|
+
_local = threading.local()
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def close_all(cls, verbose: bool = False):
|
|
58
|
+
for attr in dir(cls._local):
|
|
59
|
+
if '_conn' in attr or '_db' in attr:
|
|
60
|
+
if verbose:
|
|
61
|
+
print(f'Check {attr}')
|
|
62
|
+
obj = getattr(cls._local, attr)
|
|
63
|
+
if hasattr(obj, 'close') and callable(obj.close):
|
|
64
|
+
if verbose:
|
|
65
|
+
print(f'\tClosing {attr}')
|
|
66
|
+
obj.close()
|
|
67
|
+
delattr(cls._local, attr)
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def _setup_sqlite_db(cls, conn: sqlite3.Connection):
|
|
71
|
+
conn.execute('PRAGMA journal_mode = WAL;')
|
|
72
|
+
conn.execute('PRAGMA synchronous = NORMAL;')
|
|
73
|
+
conn.execute('pragma temp_store = memory;')
|
|
74
|
+
conn.execute('pragma page_size = 32768;')
|
|
75
|
+
conn.execute("PRAGMA busy_timeout = 3000;")
|
|
76
|
+
conn.execute('PRAGMA journal_size_limit=104857600;')
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from dataclasses import MISSING, dataclass, fields, is_dataclass
|
|
5
|
+
from types import UnionType
|
|
6
|
+
from typing import Annotated, Any, Iterable, Mapping, Sequence, get_args, get_origin, get_type_hints
|
|
7
|
+
|
|
8
|
+
from .table_spec import Col, TableInfo
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(slots=True)
|
|
12
|
+
class ColumnInfo:
|
|
13
|
+
name: str
|
|
14
|
+
python_type: Any
|
|
15
|
+
optional: bool
|
|
16
|
+
auto_increment: bool
|
|
17
|
+
has_default: bool
|
|
18
|
+
default_value: Any
|
|
19
|
+
has_default_factory: bool
|
|
20
|
+
default_factory: Any | None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(slots=True)
|
|
24
|
+
class RelationInfo:
|
|
25
|
+
name: str
|
|
26
|
+
target: type[Any]
|
|
27
|
+
many: bool
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(slots=True)
|
|
31
|
+
class ForeignKeyInfo:
|
|
32
|
+
local_columns: tuple[str, ...]
|
|
33
|
+
remote_model: type[Any]
|
|
34
|
+
remote_columns: tuple[str, ...]
|
|
35
|
+
backref_attribute: str | None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(slots=True)
|
|
39
|
+
class ModelInfo:
|
|
40
|
+
model: type[Any]
|
|
41
|
+
columns: list[ColumnInfo]
|
|
42
|
+
relations: list[RelationInfo]
|
|
43
|
+
primary_key: tuple[str, ...]
|
|
44
|
+
indexes: list[tuple[str, ...]]
|
|
45
|
+
unique_indexes: list[tuple[str, ...]]
|
|
46
|
+
foreign_keys: list[ForeignKeyInfo]
|
|
47
|
+
datasource: 'DataSourceConfig'
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(slots=True)
|
|
51
|
+
class DataSourceConfig:
|
|
52
|
+
provider: str
|
|
53
|
+
url: str | None
|
|
54
|
+
name: str | None = None
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def key(self) -> str:
|
|
58
|
+
return self.name or self.provider
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(slots=True)
|
|
62
|
+
class FieldSpec:
|
|
63
|
+
name: str
|
|
64
|
+
kind: str
|
|
65
|
+
target: type[Any] | None = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class RelationAttribute:
|
|
69
|
+
def __init__(self, model: type[Any], attribute: str) -> None:
|
|
70
|
+
self.model = model
|
|
71
|
+
self.attribute = attribute
|
|
72
|
+
|
|
73
|
+
def __repr__(self) -> str: # pragma: no cover - diagnostic only
|
|
74
|
+
return f"RelationAttribute(model={self.model.__name__}, attribute={self.attribute})"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ForeignKeyComparison:
|
|
78
|
+
def __init__(self, left: Col | tuple[Col, ...], right: Col | tuple[Col, ...]) -> None:
|
|
79
|
+
self.left = left
|
|
80
|
+
self.right = right
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class _ProxyCol(Col):
|
|
84
|
+
def __eq__(self, other: object) -> ForeignKeyComparison | bool: # type: ignore[override]
|
|
85
|
+
other_col = _normalize_col(other)
|
|
86
|
+
if other_col is None:
|
|
87
|
+
return NotImplemented # type: ignore[return-value]
|
|
88
|
+
return ForeignKeyComparison(self._to_base(), other_col)
|
|
89
|
+
|
|
90
|
+
def _to_base(self) -> Col:
|
|
91
|
+
return Col(self.name, table=self.table)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class RelationProxy:
|
|
95
|
+
def __init__(self, target: type[Any]) -> None:
|
|
96
|
+
self._target = target
|
|
97
|
+
|
|
98
|
+
def __getattr__(self, name: str) -> _ProxyCol:
|
|
99
|
+
return _ProxyCol(name, table=self._target)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class FakeSelf:
|
|
103
|
+
def __init__(self, model: type[Any], specs: Mapping[str, FieldSpec]) -> None:
|
|
104
|
+
self._model = model
|
|
105
|
+
self._specs = specs
|
|
106
|
+
|
|
107
|
+
def __getattr__(self, name: str) -> _ProxyCol | RelationProxy:
|
|
108
|
+
spec = self._specs.get(name)
|
|
109
|
+
if spec is None:
|
|
110
|
+
raise AttributeError(name)
|
|
111
|
+
if spec.kind == "column":
|
|
112
|
+
return _ProxyCol(name, table=self._model)
|
|
113
|
+
if spec.kind in {"relation", "relation_many"} and spec.target is not None:
|
|
114
|
+
return RelationProxy(spec.target)
|
|
115
|
+
raise AttributeError(name)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def inspect_models(models: Sequence[type[Any]]) -> dict[str, ModelInfo]:
|
|
119
|
+
registry: dict[str, type[Any]] = {model.__name__: model for model in models}
|
|
120
|
+
globalns: dict[str, Any] = {}
|
|
121
|
+
module_map: dict[type[Any], Any] = {}
|
|
122
|
+
for model in models:
|
|
123
|
+
module = sys.modules.get(model.__module__)
|
|
124
|
+
if module is None:
|
|
125
|
+
module = __import__(model.__module__, fromlist=["*"])
|
|
126
|
+
module_map[model] = module
|
|
127
|
+
globalns.update(vars(module))
|
|
128
|
+
globalns.update(registry)
|
|
129
|
+
|
|
130
|
+
annotations_map: dict[type[Any], dict[str, Any]] = {}
|
|
131
|
+
field_specs_map: dict[type[Any], dict[str, FieldSpec]] = {}
|
|
132
|
+
relation_map: dict[type[Any], list[RelationInfo]] = {}
|
|
133
|
+
for model in models:
|
|
134
|
+
annotations = get_type_hints(model, globalns=globalns, include_extras=True)
|
|
135
|
+
annotations_map[model] = annotations
|
|
136
|
+
columns, relations, specs = _categorize_fields(model, annotations, registry)
|
|
137
|
+
field_specs_map[model] = specs
|
|
138
|
+
relation_map[model] = relations
|
|
139
|
+
|
|
140
|
+
datasource_map: dict[type[Any], DataSourceConfig] = {
|
|
141
|
+
model: _module_datasource(module_map[model]) for model in models
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
backref_records: list[tuple[type[Any], str, Any, bool]] = []
|
|
145
|
+
for model, relations in relation_map.items():
|
|
146
|
+
for relation in relations:
|
|
147
|
+
has_attr = hasattr(model, relation.name)
|
|
148
|
+
previous = getattr(model, relation.name, None)
|
|
149
|
+
setattr(model, relation.name, RelationAttribute(model, relation.name))
|
|
150
|
+
backref_records.append((model, relation.name, previous, has_attr))
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
infos: dict[str, ModelInfo] = {}
|
|
154
|
+
for model in models:
|
|
155
|
+
annotations = annotations_map[model]
|
|
156
|
+
columns, relations, specs = _categorize_fields(model, annotations, registry)
|
|
157
|
+
table_info = TableInfo.from_dc(model)
|
|
158
|
+
primary_key = _col_names(table_info.primary_key.cols)
|
|
159
|
+
indexes: list[tuple[str, ...]] = []
|
|
160
|
+
unique_indexes: list[tuple[str, ...]] = []
|
|
161
|
+
for spec in table_info.index:
|
|
162
|
+
col_names = _col_names(spec.cols)
|
|
163
|
+
if spec.is_unique_index:
|
|
164
|
+
unique_indexes.append(col_names)
|
|
165
|
+
else:
|
|
166
|
+
indexes.append(col_names)
|
|
167
|
+
foreign_keys = _extract_foreign_keys(model, field_specs_map[model])
|
|
168
|
+
infos[model.__name__] = ModelInfo(
|
|
169
|
+
model=model,
|
|
170
|
+
columns=columns,
|
|
171
|
+
relations=relations,
|
|
172
|
+
primary_key=primary_key,
|
|
173
|
+
indexes=indexes,
|
|
174
|
+
unique_indexes=unique_indexes,
|
|
175
|
+
foreign_keys=foreign_keys,
|
|
176
|
+
datasource=datasource_map[model],
|
|
177
|
+
)
|
|
178
|
+
return infos
|
|
179
|
+
finally:
|
|
180
|
+
for model, attr, previous, has_attr in backref_records:
|
|
181
|
+
if has_attr:
|
|
182
|
+
setattr(model, attr, previous)
|
|
183
|
+
else:
|
|
184
|
+
delattr(model, attr)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _categorize_fields(
|
|
188
|
+
model: type[Any],
|
|
189
|
+
annotations: Mapping[str, Any],
|
|
190
|
+
registry: Mapping[str, type[Any]],
|
|
191
|
+
) -> tuple[list[ColumnInfo], list[RelationInfo], dict[str, FieldSpec]]:
|
|
192
|
+
columns: list[ColumnInfo] = []
|
|
193
|
+
relations: list[RelationInfo] = []
|
|
194
|
+
specs: dict[str, FieldSpec] = {}
|
|
195
|
+
|
|
196
|
+
table_info = TableInfo.from_dc(model)
|
|
197
|
+
pk_cols = set(_col_names(table_info.primary_key.cols))
|
|
198
|
+
|
|
199
|
+
for field in fields(model):
|
|
200
|
+
name = field.name
|
|
201
|
+
annotation = annotations.get(name)
|
|
202
|
+
if annotation is None:
|
|
203
|
+
continue
|
|
204
|
+
optional_flag = False
|
|
205
|
+
annotation, optional_flag = _strip_optional(annotation)
|
|
206
|
+
base_annotation = _unwrap_annotation(annotation)
|
|
207
|
+
if _is_relationship(base_annotation, registry):
|
|
208
|
+
target = _resolve_model(base_annotation, registry)
|
|
209
|
+
many = _is_collection_type(annotation)
|
|
210
|
+
relations.append(RelationInfo(name=name, target=target, many=many))
|
|
211
|
+
specs[name] = FieldSpec(name=name, kind="relation_many" if many else "relation", target=target)
|
|
212
|
+
continue
|
|
213
|
+
has_default_value = field.default is not MISSING
|
|
214
|
+
has_default_factory = field.default_factory is not MISSING
|
|
215
|
+
columns.append(
|
|
216
|
+
ColumnInfo(
|
|
217
|
+
name=name,
|
|
218
|
+
python_type=annotations[name],
|
|
219
|
+
optional=optional_flag or has_default_value or has_default_factory,
|
|
220
|
+
auto_increment=_is_auto_increment(name, annotations[name], pk_cols),
|
|
221
|
+
has_default=has_default_value,
|
|
222
|
+
default_value=field.default if has_default_value else None,
|
|
223
|
+
has_default_factory=has_default_factory,
|
|
224
|
+
default_factory=field.default_factory if has_default_factory else None,
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
specs[name] = FieldSpec(name=name, kind="column")
|
|
228
|
+
|
|
229
|
+
return columns, relations, specs
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _strip_optional(tp: Any) -> tuple[Any, bool]:
|
|
233
|
+
origin = get_origin(tp)
|
|
234
|
+
if origin is UnionType:
|
|
235
|
+
args = get_args(tp)
|
|
236
|
+
non_none = tuple(arg for arg in args if arg is not type(None)) # noqa: E721
|
|
237
|
+
is_optional = len(non_none) < len(args)
|
|
238
|
+
if len(non_none) == 1:
|
|
239
|
+
return non_none[0], is_optional
|
|
240
|
+
return tp, is_optional
|
|
241
|
+
return tp, False
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _unwrap_annotation(tp: Any) -> Any:
|
|
245
|
+
while get_origin(tp) is Annotated:
|
|
246
|
+
tp = get_args(tp)[0]
|
|
247
|
+
if _is_collection_type(tp):
|
|
248
|
+
args = get_args(tp)
|
|
249
|
+
if args:
|
|
250
|
+
return args[0]
|
|
251
|
+
return tp
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _is_collection_type(tp: Any) -> bool:
|
|
255
|
+
origin = get_origin(tp)
|
|
256
|
+
return origin in (list, set, frozenset, tuple)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _is_relationship(tp: Any, registry: Mapping[str, type[Any]]) -> bool:
|
|
260
|
+
if isinstance(tp, type) and is_dataclass(tp):
|
|
261
|
+
return True
|
|
262
|
+
if isinstance(tp, str) and tp in registry:
|
|
263
|
+
return True
|
|
264
|
+
return False
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _resolve_model(tp: Any, registry: Mapping[str, type[Any]]) -> type[Any]:
|
|
268
|
+
if isinstance(tp, type) and is_dataclass(tp):
|
|
269
|
+
return tp
|
|
270
|
+
if isinstance(tp, str):
|
|
271
|
+
model = registry.get(tp)
|
|
272
|
+
if model is None:
|
|
273
|
+
raise KeyError(f"Unknown model reference: {tp}")
|
|
274
|
+
return model
|
|
275
|
+
raise TypeError(f"Unsupported model type: {tp!r}")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _is_auto_increment(name: str, annotation: Any, pk_cols: set[str]) -> bool:
|
|
279
|
+
if name != "id" or name not in pk_cols:
|
|
280
|
+
return False
|
|
281
|
+
base, _ = _strip_optional(annotation)
|
|
282
|
+
base = _unwrap_annotation(base)
|
|
283
|
+
return base is int
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _normalize_col(value: object) -> Col | tuple[Col, ...] | None:
|
|
287
|
+
if isinstance(value, _ProxyCol):
|
|
288
|
+
return value._to_base()
|
|
289
|
+
if isinstance(value, Col):
|
|
290
|
+
return value
|
|
291
|
+
if isinstance(value, tuple):
|
|
292
|
+
cols = []
|
|
293
|
+
for item in value:
|
|
294
|
+
col = _normalize_col(item)
|
|
295
|
+
if not isinstance(col, Col):
|
|
296
|
+
return None
|
|
297
|
+
cols.append(col)
|
|
298
|
+
return tuple(cols)
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _col_names(cols: Col | tuple[Col, ...]) -> tuple[str, ...]:
|
|
303
|
+
if isinstance(cols, Col):
|
|
304
|
+
return (cols.name,)
|
|
305
|
+
return tuple(col.name for col in cols)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _extract_foreign_keys(model: type[Any], specs: Mapping[str, FieldSpec]) -> list[ForeignKeyInfo]:
|
|
309
|
+
if not hasattr(model, "foreign_key"):
|
|
310
|
+
return []
|
|
311
|
+
fake = FakeSelf(model, specs)
|
|
312
|
+
fn = getattr(model, "foreign_key")
|
|
313
|
+
results = fn(fake)
|
|
314
|
+
entries = _iterate_results(results)
|
|
315
|
+
foreign_keys: list[ForeignKeyInfo] = []
|
|
316
|
+
for entry in entries:
|
|
317
|
+
if not isinstance(entry, tuple) or len(entry) != 2:
|
|
318
|
+
raise TypeError("foreign_key must yield tuples of (comparison, backref)")
|
|
319
|
+
comparison, backref = entry
|
|
320
|
+
if not isinstance(comparison, ForeignKeyComparison):
|
|
321
|
+
raise TypeError("foreign_key comparison must be column equality")
|
|
322
|
+
local_cols, remote_cols = _determine_direction(model, comparison)
|
|
323
|
+
backref_attr: str | None = None
|
|
324
|
+
remote_model = remote_cols[0].table
|
|
325
|
+
if isinstance(backref, RelationAttribute):
|
|
326
|
+
backref_attr = backref.attribute
|
|
327
|
+
remote_model = backref.model
|
|
328
|
+
foreign_keys.append(
|
|
329
|
+
ForeignKeyInfo(
|
|
330
|
+
local_columns=tuple(col.name for col in local_cols),
|
|
331
|
+
remote_model=remote_model,
|
|
332
|
+
remote_columns=tuple(col.name for col in remote_cols),
|
|
333
|
+
backref_attribute=backref_attr,
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
return foreign_keys
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _iterate_results(results: Any) -> Iterable[Any]:
|
|
340
|
+
if results is None:
|
|
341
|
+
return []
|
|
342
|
+
if isinstance(results, Iterable) and not isinstance(results, (str, bytes)):
|
|
343
|
+
return results
|
|
344
|
+
return [results]
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _determine_direction(model: type[Any], comparison: ForeignKeyComparison) -> tuple[list[Col], list[Col]]:
|
|
348
|
+
left_cols = _ensure_sequence(comparison.left)
|
|
349
|
+
right_cols = _ensure_sequence(comparison.right)
|
|
350
|
+
left_local = all(col.table is model for col in left_cols)
|
|
351
|
+
right_local = all(col.table is model for col in right_cols)
|
|
352
|
+
if left_local and not right_local:
|
|
353
|
+
return left_cols, right_cols
|
|
354
|
+
if right_local and not left_local:
|
|
355
|
+
return right_cols, left_cols
|
|
356
|
+
raise ValueError("Unable to determine foreign key direction")
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _ensure_sequence(value: Col | tuple[Col, ...]) -> list[Col]:
|
|
360
|
+
if isinstance(value, Col):
|
|
361
|
+
return [value]
|
|
362
|
+
return list(value)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _module_datasource(module: Any | None) -> DataSourceConfig:
|
|
366
|
+
if module is None:
|
|
367
|
+
raise ValueError("Model module is not available while resolving datasource")
|
|
368
|
+
config = getattr(module, "__datasource__", None)
|
|
369
|
+
if not isinstance(config, Mapping):
|
|
370
|
+
raise ValueError(
|
|
371
|
+
f"Module {module.__name__} must define __datasource__ = "
|
|
372
|
+
"{'provider': 'sqlite', 'url': 'sqlite:///example.db'}"
|
|
373
|
+
)
|
|
374
|
+
if "provider" not in config:
|
|
375
|
+
raise ValueError(
|
|
376
|
+
f"Module {module.__name__} __datasource__ must declare a 'provider' key"
|
|
377
|
+
)
|
|
378
|
+
provider = str(config["provider"])
|
|
379
|
+
raw_url = config.get("url")
|
|
380
|
+
url = str(raw_url) if raw_url is not None else None
|
|
381
|
+
raw_name = config.get("name")
|
|
382
|
+
name = str(raw_name) if raw_name is not None else None
|
|
383
|
+
return DataSourceConfig(provider=provider, url=url, name=name)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Mapping, Sequence
|
|
4
|
+
|
|
5
|
+
from ..model_inspector import ModelInfo, inspect_models
|
|
6
|
+
from .base import DatabasePusher, ExistingColumn, SchemaDiff, SchemaPlan
|
|
7
|
+
from .sqlite import SQLITE_PUSHER, SQLitePusher, push_sqlite, _build_sqlite_schema
|
|
8
|
+
|
|
9
|
+
_PUSHER_REGISTRY: dict[str, DatabasePusher] = {
|
|
10
|
+
"sqlite": SQLITE_PUSHER,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def register_pusher(provider: str, pusher: DatabasePusher) -> None:
|
|
15
|
+
_PUSHER_REGISTRY[provider] = pusher
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_pusher(provider: str) -> DatabasePusher:
|
|
19
|
+
if provider not in _PUSHER_REGISTRY:
|
|
20
|
+
raise ValueError(f"Unsupported provider: {provider}")
|
|
21
|
+
return _PUSHER_REGISTRY[provider]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def db_push(
|
|
25
|
+
models: Sequence[type[Any]],
|
|
26
|
+
connections: Mapping[str, Any],
|
|
27
|
+
*,
|
|
28
|
+
sync_indexes: bool = False,
|
|
29
|
+
confirm_rebuild: Callable[[ModelInfo, SchemaPlan, tuple[ExistingColumn, ...] | None, SchemaDiff], bool] | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
model_infos = inspect_models(models)
|
|
32
|
+
grouped: dict[str, dict[str, list[ModelInfo]]] = {}
|
|
33
|
+
for info in model_infos.values():
|
|
34
|
+
provider = info.datasource.provider
|
|
35
|
+
key = info.datasource.key
|
|
36
|
+
provider_map = grouped.setdefault(provider, {})
|
|
37
|
+
provider_map.setdefault(key, []).append(info)
|
|
38
|
+
|
|
39
|
+
for provider, key_map in grouped.items():
|
|
40
|
+
pusher = get_pusher(provider)
|
|
41
|
+
for key, infos in key_map.items():
|
|
42
|
+
if key not in connections:
|
|
43
|
+
raise KeyError(f"Connection for datasource '{key}' is missing")
|
|
44
|
+
connection = connections[key]
|
|
45
|
+
pusher.push(
|
|
46
|
+
connection,
|
|
47
|
+
infos,
|
|
48
|
+
sync_indexes=sync_indexes,
|
|
49
|
+
confirm_rebuild=confirm_rebuild,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"db_push",
|
|
55
|
+
"get_pusher",
|
|
56
|
+
"register_pusher",
|
|
57
|
+
"SQLitePusher",
|
|
58
|
+
"push_sqlite",
|
|
59
|
+
"_build_sqlite_schema",
|
|
60
|
+
]
|