dclassql 0.3.0__tar.gz → 0.4.0__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.
- {dclassql-0.3.0 → dclassql-0.4.0}/PKG-INFO +1 -1
- {dclassql-0.3.0 → dclassql-0.4.0}/pyproject.toml +1 -1
- dclassql-0.4.0/src/dclassql/__init__.py +21 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/cli.py +31 -13
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/codegen.py +55 -47
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/model_inspector.py +1 -1
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/runtime/backends/protocols.py +2 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/runtime/backends/where_compiler.py +9 -5
- dclassql-0.4.0/src/dclassql/templates/partials/client_class.jinja +60 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/templates/partials/imports.jinja +2 -2
- dclassql-0.3.0/src/dclassql/__init__.py +0 -34
- dclassql-0.3.0/src/dclassql/asdict.pyi +0 -57
- dclassql-0.3.0/src/dclassql/templates/partials/client_class.jinja +0 -32
- {dclassql-0.3.0 → dclassql-0.4.0}/README.md +0 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/.gitignore +0 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/asdict.py +0 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/db_pool.py +0 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/push/__init__.py +0 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/push/base.py +0 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/push/sqlite.py +0 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/runtime/backends/__init__.py +0 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/runtime/backends/base.py +0 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/runtime/backends/lazy.py +0 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/runtime/backends/metadata.py +0 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/runtime/backends/sqlite.py +0 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/runtime/datasource.py +0 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/runtime/sql_recorder.py +0 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/runtime/sqlite_adapters.py +0 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/table_spec.py +0 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/templates/__init__.py +0 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/templates/asdict_stub.pyi.jinja +0 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/templates/client_module.py.jinja +0 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/templates/partials/exports.jinja +0 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/templates/partials/macros.jinja +0 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/templates/partials/model_section.jinja +0 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/templates/partials/scalar_filters.jinja +0 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/typing.py +0 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/unwarp.py +0 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/utils/__init__.py +0 -0
- {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/utils/ensure.py +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .asdict import asdict
|
|
2
|
+
from .db_pool import BaseDBPool, save_local
|
|
3
|
+
from .model_inspector import DataSourceConfig
|
|
4
|
+
from .push import db_push
|
|
5
|
+
from .runtime.backends.lazy import eager
|
|
6
|
+
from .runtime.sql_recorder import record_sql
|
|
7
|
+
from .unwarp import unwarp, unwarp_or, unwarp_or_raise
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
'db_push',
|
|
12
|
+
'eager',
|
|
13
|
+
'asdict',
|
|
14
|
+
'unwarp',
|
|
15
|
+
'unwarp_or',
|
|
16
|
+
'unwarp_or_raise',
|
|
17
|
+
'BaseDBPool',
|
|
18
|
+
'save_local',
|
|
19
|
+
'DataSourceConfig',
|
|
20
|
+
'record_sql',
|
|
21
|
+
]
|
|
@@ -18,6 +18,7 @@ from .runtime.datasource import open_sqlite_connection
|
|
|
18
18
|
DEFAULT_MODEL_FILE = "model.py"
|
|
19
19
|
GENERATED_CLIENT_FILENAME = "client.py"
|
|
20
20
|
|
|
21
|
+
GenerateTarget = Literal["model-dir", "package"]
|
|
21
22
|
ConfirmRebuildMode = Literal["auto", "prompt"]
|
|
22
23
|
ConfirmCallback = Callable[
|
|
23
24
|
[ModelInfo, SchemaPlan, tuple[ExistingColumn, ...] | None, SchemaDiff],
|
|
@@ -66,12 +67,21 @@ def _find_package_directory() -> Path:
|
|
|
66
67
|
return Path(next(iter(spec.submodule_search_locations))).resolve()
|
|
67
68
|
|
|
68
69
|
|
|
69
|
-
def
|
|
70
|
-
|
|
70
|
+
def resolve_client_package_name(module_path: Path) -> str:
|
|
71
|
+
stem = re.sub(r"[^0-9a-zA-Z_]+", "_", module_path.stem) or "_"
|
|
72
|
+
return f"{stem}_client"
|
|
71
73
|
|
|
72
74
|
|
|
73
|
-
def
|
|
74
|
-
|
|
75
|
+
def resolve_client_class_name(module_path: Path) -> str:
|
|
76
|
+
package_name = resolve_client_package_name(module_path)
|
|
77
|
+
return "".join(part.capitalize() for part in package_name.split("_") if part)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def resolve_generated_package_dir(module_path: Path, target: GenerateTarget = "model-dir") -> Path:
|
|
81
|
+
package_name = resolve_client_package_name(module_path)
|
|
82
|
+
if target == "model-dir":
|
|
83
|
+
return module_path.resolve().parent / package_name
|
|
84
|
+
return _find_package_directory() / package_name
|
|
75
85
|
|
|
76
86
|
|
|
77
87
|
def collect_models(module: ModuleType) -> list[type[Any]]:
|
|
@@ -162,16 +172,18 @@ def push_database(
|
|
|
162
172
|
pass
|
|
163
173
|
|
|
164
174
|
|
|
165
|
-
def command_generate(module_path: Path) -> None:
|
|
175
|
+
def command_generate(module_path: Path, *, target: GenerateTarget = "model-dir") -> None:
|
|
166
176
|
module = load_module(module_path)
|
|
167
177
|
models = collect_models(module)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
178
|
+
client_class_name = resolve_client_class_name(module_path)
|
|
179
|
+
generated = generate_client(models, client_class_name=client_class_name)
|
|
180
|
+
output_dir = resolve_generated_package_dir(module_path, target)
|
|
181
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
182
|
+
(output_dir / "__init__.py").write_text(generated.init_code, encoding="utf-8")
|
|
183
|
+
(output_dir / "__init__.pyi").write_text(generated.init_stub, encoding="utf-8")
|
|
184
|
+
(output_dir / GENERATED_CLIENT_FILENAME).write_text(generated.code, encoding="utf-8")
|
|
185
|
+
(output_dir / "asdict.pyi").write_text(generated.asdict_stub, encoding="utf-8")
|
|
186
|
+
sys.stdout.write(f"Client package written to {output_dir}\n")
|
|
175
187
|
|
|
176
188
|
|
|
177
189
|
def command_push_db(
|
|
@@ -197,7 +209,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
197
209
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
198
210
|
|
|
199
211
|
generate_parser = subparsers.add_parser("generate", help="Generate client code for given models")
|
|
200
|
-
generate_parser.
|
|
212
|
+
generate_parser.add_argument(
|
|
213
|
+
"--target",
|
|
214
|
+
choices=("model-dir", "package"),
|
|
215
|
+
default="model-dir",
|
|
216
|
+
help="生成 client 的位置: model-dir 写到模型文件同目录; package 写到 dclassql 包内",
|
|
217
|
+
)
|
|
218
|
+
generate_parser.set_defaults(handler=lambda args: command_generate(args.module, target=args.target))
|
|
201
219
|
|
|
202
220
|
push_parser = subparsers.add_parser("push-db", help="Apply schema and indexes to configured databases")
|
|
203
221
|
push_parser.add_argument(
|
|
@@ -17,7 +17,10 @@ from .model_inspector import ColumnInfo, ModelInfo, inspect_models, DataSourceCo
|
|
|
17
17
|
class GeneratedModule:
|
|
18
18
|
code: str
|
|
19
19
|
asdict_stub: str
|
|
20
|
+
init_code: str
|
|
21
|
+
init_stub: str
|
|
20
22
|
model_names: tuple[str, ...]
|
|
23
|
+
client_class_name: str
|
|
21
24
|
|
|
22
25
|
|
|
23
26
|
@dataclass(slots=True)
|
|
@@ -131,7 +134,7 @@ class ModelRenderContext:
|
|
|
131
134
|
|
|
132
135
|
|
|
133
136
|
@dataclass(slots=True)
|
|
134
|
-
class
|
|
137
|
+
class ClientDataSourceContext:
|
|
135
138
|
key: str
|
|
136
139
|
key_repr: str
|
|
137
140
|
provider_repr: str
|
|
@@ -139,24 +142,16 @@ class ClientDatasourceContext:
|
|
|
139
142
|
name_repr: str
|
|
140
143
|
|
|
141
144
|
|
|
142
|
-
@dataclass(slots=True)
|
|
143
|
-
class BackendMethodContext:
|
|
144
|
-
key: str
|
|
145
|
-
key_repr: str
|
|
146
|
-
method_name: str
|
|
147
|
-
|
|
148
|
-
|
|
149
145
|
@dataclass(slots=True)
|
|
150
146
|
class ClientModelBindingContext:
|
|
151
147
|
attr_name: str
|
|
152
148
|
model_name: str
|
|
153
|
-
backend_method: str
|
|
154
149
|
|
|
155
150
|
|
|
156
151
|
@dataclass(slots=True)
|
|
157
152
|
class ClientContext:
|
|
158
|
-
|
|
159
|
-
|
|
153
|
+
class_name: str
|
|
154
|
+
datasource: ClientDataSourceContext
|
|
160
155
|
model_bindings: tuple[ClientModelBindingContext, ...]
|
|
161
156
|
|
|
162
157
|
|
|
@@ -176,7 +171,7 @@ def _get_environment() -> Environment:
|
|
|
176
171
|
return _ENVIRONMENT
|
|
177
172
|
|
|
178
173
|
|
|
179
|
-
def generate_client(models: Sequence[type[Any]]) -> GeneratedModule:
|
|
174
|
+
def generate_client(models: Sequence[type[Any]], *, client_class_name: str = "GeneratedClient") -> GeneratedModule:
|
|
180
175
|
model_infos = inspect_models(models)
|
|
181
176
|
renderer = _TypeRenderer({info.model: name for name, info in model_infos.items()})
|
|
182
177
|
filter_registry = _ScalarFilterRegistry(renderer)
|
|
@@ -201,8 +196,8 @@ def generate_client(models: Sequence[type[Any]]) -> GeneratedModule:
|
|
|
201
196
|
for module, names in sorted(combined_imports.items())
|
|
202
197
|
]
|
|
203
198
|
|
|
204
|
-
client_context = _build_client_context(model_infos)
|
|
205
|
-
exports = _collect_exports(model_contexts)
|
|
199
|
+
client_context = _build_client_context(model_infos, client_class_name)
|
|
200
|
+
exports = _collect_exports(model_contexts, client_class_name)
|
|
206
201
|
scalar_filters = filter_registry.render_definitions()
|
|
207
202
|
|
|
208
203
|
template = _get_environment().get_template(_TEMPLATE_NAME)
|
|
@@ -216,7 +211,16 @@ def generate_client(models: Sequence[type[Any]]) -> GeneratedModule:
|
|
|
216
211
|
if not code.endswith("\n"):
|
|
217
212
|
code += "\n"
|
|
218
213
|
asdict_stub = _render_asdict_stub(model_contexts)
|
|
219
|
-
|
|
214
|
+
init_code = _render_init_code(client_class_name)
|
|
215
|
+
init_stub = _render_init_stub(client_class_name)
|
|
216
|
+
return GeneratedModule(
|
|
217
|
+
code=code,
|
|
218
|
+
asdict_stub=asdict_stub,
|
|
219
|
+
init_code=init_code,
|
|
220
|
+
init_stub=init_stub,
|
|
221
|
+
model_names=tuple(sorted(model_infos.keys())),
|
|
222
|
+
client_class_name=client_class_name,
|
|
223
|
+
)
|
|
220
224
|
|
|
221
225
|
|
|
222
226
|
def _build_model_context(
|
|
@@ -418,53 +422,39 @@ def _build_model_context(
|
|
|
418
422
|
)
|
|
419
423
|
|
|
420
424
|
|
|
421
|
-
def _build_client_context(model_infos: Mapping[str, ModelInfo]) -> ClientContext:
|
|
422
|
-
datasource_configs
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
existing = datasource_configs.get(key)
|
|
427
|
-
if existing is None:
|
|
428
|
-
datasource_configs[key] = datasource
|
|
429
|
-
elif existing != datasource:
|
|
430
|
-
raise ValueError(f"Conflicting datasource key '{key}' for providers")
|
|
431
|
-
|
|
432
|
-
datasource_items = [
|
|
433
|
-
ClientDatasourceContext(
|
|
434
|
-
key=key,
|
|
435
|
-
key_repr=repr(key),
|
|
436
|
-
provider_repr=repr(ds.provider),
|
|
437
|
-
url_repr=repr(ds.url),
|
|
438
|
-
name_repr=repr(ds.name),
|
|
425
|
+
def _build_client_context(model_infos: Mapping[str, ModelInfo], client_class_name: str) -> ClientContext:
|
|
426
|
+
datasource_configs = {info.datasource for info in model_infos.values()}
|
|
427
|
+
if len(datasource_configs) != 1:
|
|
428
|
+
labels = ", ".join(
|
|
429
|
+
f"{ds.key}({ds.provider}, {ds.url!r})" for ds in sorted(datasource_configs, key=lambda item: item.key)
|
|
439
430
|
)
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
431
|
+
raise ValueError(f"Generated Client can only use one datasource, got: {labels}")
|
|
432
|
+
datasource = next(iter(datasource_configs))
|
|
433
|
+
datasource_item = ClientDataSourceContext(
|
|
434
|
+
key=datasource.key,
|
|
435
|
+
key_repr=repr(datasource.key),
|
|
436
|
+
provider_repr=repr(datasource.provider),
|
|
437
|
+
url_repr=repr(datasource.url),
|
|
438
|
+
name_repr=repr(datasource.name),
|
|
439
|
+
)
|
|
449
440
|
|
|
450
441
|
model_bindings = [
|
|
451
442
|
ClientModelBindingContext(
|
|
452
443
|
attr_name=_camel_to_snake(name),
|
|
453
444
|
model_name=name,
|
|
454
|
-
backend_method=method_map[(model_infos[name].datasource.name or model_infos[name].datasource.provider)],
|
|
455
445
|
)
|
|
456
446
|
for name in sorted(model_infos.keys())
|
|
457
447
|
]
|
|
458
448
|
|
|
459
449
|
return ClientContext(
|
|
460
|
-
|
|
461
|
-
|
|
450
|
+
class_name=client_class_name,
|
|
451
|
+
datasource=datasource_item,
|
|
462
452
|
model_bindings=tuple(model_bindings),
|
|
463
453
|
)
|
|
464
454
|
|
|
465
455
|
|
|
466
|
-
def _collect_exports(model_contexts: Sequence[ModelRenderContext]) -> list[str]:
|
|
467
|
-
exports: list[str] = ["DataSourceConfig", "ForeignKeySpec",
|
|
456
|
+
def _collect_exports(model_contexts: Sequence[ModelRenderContext], client_class_name: str) -> list[str]:
|
|
457
|
+
exports: list[str] = ["DataSourceConfig", "ForeignKeySpec", client_class_name]
|
|
468
458
|
for context in model_contexts:
|
|
469
459
|
name = context.name
|
|
470
460
|
exports.extend(
|
|
@@ -496,6 +486,24 @@ def _render_asdict_stub(model_contexts: Sequence[ModelRenderContext]) -> str:
|
|
|
496
486
|
return code
|
|
497
487
|
|
|
498
488
|
|
|
489
|
+
def _render_init_code(client_class_name: str) -> str:
|
|
490
|
+
code = (
|
|
491
|
+
"from dclassql.asdict import asdict as asdict\n"
|
|
492
|
+
f"from .client import {client_class_name} as {client_class_name}\n\n"
|
|
493
|
+
f"__all__ = ['{client_class_name}', 'asdict']\n"
|
|
494
|
+
)
|
|
495
|
+
return code
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _render_init_stub(client_class_name: str) -> str:
|
|
499
|
+
code = (
|
|
500
|
+
"from .asdict import asdict as asdict\n"
|
|
501
|
+
f"from .client import {client_class_name} as {client_class_name}\n\n"
|
|
502
|
+
"__all__: list[str]\n"
|
|
503
|
+
)
|
|
504
|
+
return code
|
|
505
|
+
|
|
506
|
+
|
|
499
507
|
def _build_relation_entries(info: ModelInfo, model_infos: Mapping[str, ModelInfo]) -> list[dict[str, Any]]:
|
|
500
508
|
entries: list[dict[str, Any]] = []
|
|
501
509
|
if not info.relations:
|
|
@@ -150,6 +150,8 @@ class BackendProtocol(Protocol):
|
|
|
150
150
|
|
|
151
151
|
def execute_raw(self, sql: str, params: Sequence[object] | None = None, auto_commit: bool = True) -> int: ...
|
|
152
152
|
|
|
153
|
+
def close(self) -> None: ...
|
|
154
|
+
|
|
153
155
|
def escape_identifier(self, name: str) -> str: ...
|
|
154
156
|
|
|
155
157
|
def new_parameter(self) -> Parameter: ...
|
|
@@ -2,11 +2,11 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import importlib
|
|
4
4
|
from collections.abc import Mapping as ABCMapping
|
|
5
|
-
from typing import Mapping, Sequence
|
|
5
|
+
from typing import Mapping, Sequence, cast
|
|
6
6
|
|
|
7
7
|
from pypika import Query, Table
|
|
8
8
|
from pypika.queries import QueryBuilder
|
|
9
|
-
from pypika.terms import Criterion, ExistsCriterion, Field, Parameter
|
|
9
|
+
from pypika.terms import Criterion, ExistsCriterion, Field, Not, Parameter
|
|
10
10
|
|
|
11
11
|
from dclassql.typing import IncludeT, InsertT, ModelT, OrderByT, WhereT
|
|
12
12
|
from dclassql.utils.ensure import ensure_sequence, ensure_string
|
|
@@ -41,10 +41,14 @@ class EscapeLikeCriterion(Criterion):
|
|
|
41
41
|
self._escape = escape
|
|
42
42
|
self._negated = negated
|
|
43
43
|
|
|
44
|
-
def negate(self) ->
|
|
45
|
-
|
|
44
|
+
def negate(self) -> Not:
|
|
45
|
+
# PyPika 将 Term.negate() 标为 Not;这里的自定义条件只需要翻转自己的 SQL 操作符。
|
|
46
|
+
return cast(
|
|
47
|
+
Not,
|
|
48
|
+
EscapeLikeCriterion(self._field, self._parameter, escape=self._escape, negated=not self._negated),
|
|
49
|
+
)
|
|
46
50
|
|
|
47
|
-
def get_sql(self, **kwargs: object) -> str:
|
|
51
|
+
def get_sql(self, **kwargs: object) -> str:
|
|
48
52
|
field_sql = self._field.get_sql(**kwargs)
|
|
49
53
|
param_sql = self._parameter.get_sql(**kwargs)
|
|
50
54
|
operator = "NOT LIKE" if self._negated else "LIKE"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
class {{ client.class_name }}(BaseDBPool):
|
|
2
|
+
datasource: DataSourceConfig = DataSourceConfig(
|
|
3
|
+
provider={{ client.datasource.provider_repr }},
|
|
4
|
+
url={{ client.datasource.url_repr }},
|
|
5
|
+
name={{ client.datasource.name_repr }},
|
|
6
|
+
)
|
|
7
|
+
datasource_key: str = {{ client.datasource.key_repr }}
|
|
8
|
+
|
|
9
|
+
def __init__(self, *, datasource: DataSourceConfig = datasource, echo_sql: bool = False) -> None:
|
|
10
|
+
self.datasource = datasource
|
|
11
|
+
self._echo_sql = echo_sql
|
|
12
|
+
self._backend_instance: BackendProtocol | None = None
|
|
13
|
+
{% for binding in client.model_bindings %} self.{{ binding.attr_name }} = {{ binding.model_name }}Table(self._backend())
|
|
14
|
+
{% endfor %}
|
|
15
|
+
|
|
16
|
+
def _backend(self) -> BackendProtocol:
|
|
17
|
+
if self._backend_instance is None:
|
|
18
|
+
self._backend_instance = self._make_backend(self.datasource)
|
|
19
|
+
return self._backend_instance
|
|
20
|
+
|
|
21
|
+
def _make_backend(self, datasource: DataSourceConfig) -> BackendProtocol:
|
|
22
|
+
if datasource.provider == 'sqlite':
|
|
23
|
+
from dclassql.runtime.backends.sqlite import SQLiteBackend
|
|
24
|
+
return SQLiteBackend(lambda: self._open_connection(datasource), echo_sql=self._echo_sql)
|
|
25
|
+
raise ValueError(f"Unsupported provider '{datasource.provider}'")
|
|
26
|
+
|
|
27
|
+
def _open_connection(self, datasource: DataSourceConfig) -> Any:
|
|
28
|
+
if datasource.provider == 'sqlite':
|
|
29
|
+
conn = open_sqlite_connection(datasource.url)
|
|
30
|
+
self._setup_sqlite_db(conn)
|
|
31
|
+
return conn
|
|
32
|
+
raise ValueError(f"Unsupported provider '{datasource.provider}'")
|
|
33
|
+
|
|
34
|
+
def push_db(
|
|
35
|
+
self,
|
|
36
|
+
*,
|
|
37
|
+
sync_indexes: bool = False,
|
|
38
|
+
force_rebuild: bool = False,
|
|
39
|
+
) -> None:
|
|
40
|
+
connection = self._open_connection(self.datasource)
|
|
41
|
+
try:
|
|
42
|
+
db_push(
|
|
43
|
+
(
|
|
44
|
+
{% for binding in client.model_bindings %} {{ binding.model_name }},
|
|
45
|
+
{% endfor %} ),
|
|
46
|
+
{self.datasource_key: connection},
|
|
47
|
+
sync_indexes=sync_indexes,
|
|
48
|
+
confirm_rebuild=(lambda *_: True) if force_rebuild else None,
|
|
49
|
+
)
|
|
50
|
+
finally:
|
|
51
|
+
connection.close()
|
|
52
|
+
|
|
53
|
+
def close(self) -> None:
|
|
54
|
+
if self._backend_instance is not None:
|
|
55
|
+
self._backend_instance.close()
|
|
56
|
+
self._backend_instance = None
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def close_all(cls, verbose: bool = False) -> None:
|
|
60
|
+
super().close_all(verbose=verbose)
|
|
@@ -6,8 +6,8 @@ from types import MappingProxyType
|
|
|
6
6
|
from typing import Any, Literal, Mapping, Sequence, NotRequired, overload
|
|
7
7
|
from typing_extensions import TypedDict
|
|
8
8
|
|
|
9
|
-
from dclassql import DataSourceConfig
|
|
10
|
-
from dclassql.db_pool import BaseDBPool
|
|
9
|
+
from dclassql import DataSourceConfig, db_push
|
|
10
|
+
from dclassql.db_pool import BaseDBPool
|
|
11
11
|
from dclassql.runtime.backends import BackendProtocol, ColumnSpec, ForeignKeySpec, RelationSpec
|
|
12
12
|
from dclassql.runtime.backends.protocols import TableProtocol
|
|
13
13
|
from dclassql.runtime.datasource import open_sqlite_connection
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
from .asdict import asdict
|
|
2
|
-
from .db_pool import BaseDBPool, save_local
|
|
3
|
-
from .model_inspector import DataSourceConfig
|
|
4
|
-
from .push import db_push
|
|
5
|
-
from .runtime.backends.lazy import eager
|
|
6
|
-
from .runtime.sql_recorder import record_sql
|
|
7
|
-
from .unwarp import unwarp, unwarp_or, unwarp_or_raise
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class _MissingClient:
|
|
11
|
-
def __init__(self, *args: object, **kwargs: object) -> None:
|
|
12
|
-
raise RuntimeError(
|
|
13
|
-
"dclassql.Client 尚未生成。请先运行 `dclassql -m <model.py> generate` 生成客户端后再导入。"
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
try: # pragma: no cover - exercised in integration tests
|
|
17
|
-
from .client import Client # type: ignore
|
|
18
|
-
except (ModuleNotFoundError, ImportError): # pragma: no cover - fallback when未生成
|
|
19
|
-
Client = _MissingClient # type: ignore[assignment]
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
__all__ = [
|
|
23
|
-
'Client',
|
|
24
|
-
'db_push',
|
|
25
|
-
'eager',
|
|
26
|
-
'asdict',
|
|
27
|
-
'unwarp',
|
|
28
|
-
'unwarp_or',
|
|
29
|
-
'unwarp_or_raise',
|
|
30
|
-
'BaseDBPool',
|
|
31
|
-
'save_local',
|
|
32
|
-
'DataSourceConfig',
|
|
33
|
-
'record_sql',
|
|
34
|
-
]
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from collections.abc import Sequence
|
|
4
|
-
from typing import Any, Literal, overload
|
|
5
|
-
|
|
6
|
-
from .client import (
|
|
7
|
-
Address,
|
|
8
|
-
AddressDict,
|
|
9
|
-
BirthDay,
|
|
10
|
-
BirthDayDict,
|
|
11
|
-
Book,
|
|
12
|
-
BookDict,
|
|
13
|
-
User,
|
|
14
|
-
UserDict,
|
|
15
|
-
UserBook,
|
|
16
|
-
UserBookDict,
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
RelationPolicy = Literal['skip', 'fetch', 'keep']
|
|
20
|
-
|
|
21
|
-
@overload
|
|
22
|
-
def asdict(value: Address, *, relation_policy: RelationPolicy = 'keep') -> AddressDict: ...
|
|
23
|
-
|
|
24
|
-
@overload
|
|
25
|
-
def asdict(value: BirthDay, *, relation_policy: RelationPolicy = 'keep') -> BirthDayDict: ...
|
|
26
|
-
|
|
27
|
-
@overload
|
|
28
|
-
def asdict(value: Book, *, relation_policy: RelationPolicy = 'keep') -> BookDict: ...
|
|
29
|
-
|
|
30
|
-
@overload
|
|
31
|
-
def asdict(value: User, *, relation_policy: RelationPolicy = 'keep') -> UserDict: ...
|
|
32
|
-
|
|
33
|
-
@overload
|
|
34
|
-
def asdict(value: UserBook, *, relation_policy: RelationPolicy = 'keep') -> UserBookDict: ...
|
|
35
|
-
|
|
36
|
-
@overload
|
|
37
|
-
def asdict(value: Sequence[Address], *, relation_policy: RelationPolicy = 'keep') -> list[AddressDict]: ...
|
|
38
|
-
|
|
39
|
-
@overload
|
|
40
|
-
def asdict(value: Sequence[BirthDay], *, relation_policy: RelationPolicy = 'keep') -> list[BirthDayDict]: ...
|
|
41
|
-
|
|
42
|
-
@overload
|
|
43
|
-
def asdict(value: Sequence[Book], *, relation_policy: RelationPolicy = 'keep') -> list[BookDict]: ...
|
|
44
|
-
|
|
45
|
-
@overload
|
|
46
|
-
def asdict(value: Sequence[User], *, relation_policy: RelationPolicy = 'keep') -> list[UserDict]: ...
|
|
47
|
-
|
|
48
|
-
@overload
|
|
49
|
-
def asdict(value: Sequence[UserBook], *, relation_policy: RelationPolicy = 'keep') -> list[UserBookDict]: ...
|
|
50
|
-
|
|
51
|
-
@overload
|
|
52
|
-
def asdict(value: Sequence[Any], *, relation_policy: RelationPolicy = 'keep') -> list[Any]: ...
|
|
53
|
-
|
|
54
|
-
@overload
|
|
55
|
-
def asdict(value: None, *, relation_policy: RelationPolicy = 'keep') -> None: ...
|
|
56
|
-
|
|
57
|
-
def asdict(value: object, *, relation_policy: RelationPolicy = 'keep') -> Any: ...
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
class Client(BaseDBPool):
|
|
2
|
-
_echo_sql: bool = False
|
|
3
|
-
datasources = {
|
|
4
|
-
{% for item in client.datasource_items %} {{ item.key_repr }}: DataSourceConfig(provider={{ item.provider_repr }}, url={{ item.url_repr }}, name={{ item.name_repr }}),
|
|
5
|
-
{% endfor %} }
|
|
6
|
-
|
|
7
|
-
{% for method in client.backend_methods %} @classmethod
|
|
8
|
-
@save_local
|
|
9
|
-
def {{ method.method_name }}(cls, *, echo_sql: bool | None = None) -> BackendProtocol:
|
|
10
|
-
config = cls.datasources[{{ method.key_repr }}]
|
|
11
|
-
backend_echo = cls._echo_sql if echo_sql is None else echo_sql
|
|
12
|
-
if config.provider == 'sqlite':
|
|
13
|
-
from dclassql.runtime.backends.sqlite import SQLiteBackend
|
|
14
|
-
conn = open_sqlite_connection(config.url)
|
|
15
|
-
cls._setup_sqlite_db(conn)
|
|
16
|
-
return SQLiteBackend(conn, echo_sql=backend_echo)
|
|
17
|
-
raise ValueError(f"Unsupported provider '{config.provider}' for datasource {{ method.key_repr }}")
|
|
18
|
-
|
|
19
|
-
{% endfor %} def __init__(self, *, echo_sql: bool = False) -> None:
|
|
20
|
-
self._echo_sql = echo_sql
|
|
21
|
-
{% for binding in client.model_bindings %} self.{{ binding.attr_name }} = {{ binding.model_name }}Table(self.{{ binding.backend_method }}(echo_sql=echo_sql))
|
|
22
|
-
{% endfor %}
|
|
23
|
-
|
|
24
|
-
@classmethod
|
|
25
|
-
def close_all(cls, verbose: bool = False) -> None:
|
|
26
|
-
super().close_all(verbose=verbose)
|
|
27
|
-
{% for method in client.backend_methods %} if hasattr(cls._local, '{{ method.method_name }}'):
|
|
28
|
-
backend = getattr(cls._local, '{{ method.method_name }}')
|
|
29
|
-
if hasattr(backend, 'close') and callable(getattr(backend, 'close')):
|
|
30
|
-
backend.close()
|
|
31
|
-
delattr(cls._local, '{{ method.method_name }}')
|
|
32
|
-
{% endfor %}
|
|
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
|
|
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
|
|
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
|