dclassql 0.3.1__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.1 → dclassql-0.4.0}/PKG-INFO +1 -1
- {dclassql-0.3.1 → dclassql-0.4.0}/pyproject.toml +1 -1
- dclassql-0.4.0/src/dclassql/__init__.py +21 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/cli.py +31 -13
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/codegen.py +39 -7
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/templates/partials/client_class.jinja +3 -9
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/templates/partials/imports.jinja +1 -3
- dclassql-0.3.1/src/dclassql/__init__.py +0 -34
- dclassql-0.3.1/src/dclassql/asdict.pyi +0 -57
- dclassql-0.3.1/src/dclassql/client.py +0 -1397
- {dclassql-0.3.1 → dclassql-0.4.0}/README.md +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/.gitignore +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/asdict.py +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/db_pool.py +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/model_inspector.py +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/push/__init__.py +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/push/base.py +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/push/sqlite.py +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/runtime/backends/__init__.py +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/runtime/backends/base.py +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/runtime/backends/lazy.py +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/runtime/backends/metadata.py +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/runtime/backends/protocols.py +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/runtime/backends/sqlite.py +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/runtime/backends/where_compiler.py +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/runtime/datasource.py +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/runtime/sql_recorder.py +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/runtime/sqlite_adapters.py +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/table_spec.py +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/templates/__init__.py +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/templates/asdict_stub.pyi.jinja +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/templates/client_module.py.jinja +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/templates/partials/exports.jinja +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/templates/partials/macros.jinja +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/templates/partials/model_section.jinja +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/templates/partials/scalar_filters.jinja +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/typing.py +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/unwarp.py +0 -0
- {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/utils/__init__.py +0 -0
- {dclassql-0.3.1 → 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)
|
|
@@ -147,6 +150,7 @@ class ClientModelBindingContext:
|
|
|
147
150
|
|
|
148
151
|
@dataclass(slots=True)
|
|
149
152
|
class ClientContext:
|
|
153
|
+
class_name: str
|
|
150
154
|
datasource: ClientDataSourceContext
|
|
151
155
|
model_bindings: tuple[ClientModelBindingContext, ...]
|
|
152
156
|
|
|
@@ -167,7 +171,7 @@ def _get_environment() -> Environment:
|
|
|
167
171
|
return _ENVIRONMENT
|
|
168
172
|
|
|
169
173
|
|
|
170
|
-
def generate_client(models: Sequence[type[Any]]) -> GeneratedModule:
|
|
174
|
+
def generate_client(models: Sequence[type[Any]], *, client_class_name: str = "GeneratedClient") -> GeneratedModule:
|
|
171
175
|
model_infos = inspect_models(models)
|
|
172
176
|
renderer = _TypeRenderer({info.model: name for name, info in model_infos.items()})
|
|
173
177
|
filter_registry = _ScalarFilterRegistry(renderer)
|
|
@@ -192,8 +196,8 @@ def generate_client(models: Sequence[type[Any]]) -> GeneratedModule:
|
|
|
192
196
|
for module, names in sorted(combined_imports.items())
|
|
193
197
|
]
|
|
194
198
|
|
|
195
|
-
client_context = _build_client_context(model_infos)
|
|
196
|
-
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)
|
|
197
201
|
scalar_filters = filter_registry.render_definitions()
|
|
198
202
|
|
|
199
203
|
template = _get_environment().get_template(_TEMPLATE_NAME)
|
|
@@ -207,7 +211,16 @@ def generate_client(models: Sequence[type[Any]]) -> GeneratedModule:
|
|
|
207
211
|
if not code.endswith("\n"):
|
|
208
212
|
code += "\n"
|
|
209
213
|
asdict_stub = _render_asdict_stub(model_contexts)
|
|
210
|
-
|
|
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
|
+
)
|
|
211
224
|
|
|
212
225
|
|
|
213
226
|
def _build_model_context(
|
|
@@ -409,7 +422,7 @@ def _build_model_context(
|
|
|
409
422
|
)
|
|
410
423
|
|
|
411
424
|
|
|
412
|
-
def _build_client_context(model_infos: Mapping[str, ModelInfo]) -> ClientContext:
|
|
425
|
+
def _build_client_context(model_infos: Mapping[str, ModelInfo], client_class_name: str) -> ClientContext:
|
|
413
426
|
datasource_configs = {info.datasource for info in model_infos.values()}
|
|
414
427
|
if len(datasource_configs) != 1:
|
|
415
428
|
labels = ", ".join(
|
|
@@ -434,13 +447,14 @@ def _build_client_context(model_infos: Mapping[str, ModelInfo]) -> ClientContext
|
|
|
434
447
|
]
|
|
435
448
|
|
|
436
449
|
return ClientContext(
|
|
450
|
+
class_name=client_class_name,
|
|
437
451
|
datasource=datasource_item,
|
|
438
452
|
model_bindings=tuple(model_bindings),
|
|
439
453
|
)
|
|
440
454
|
|
|
441
455
|
|
|
442
|
-
def _collect_exports(model_contexts: Sequence[ModelRenderContext]) -> list[str]:
|
|
443
|
-
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]
|
|
444
458
|
for context in model_contexts:
|
|
445
459
|
name = context.name
|
|
446
460
|
exports.extend(
|
|
@@ -472,6 +486,24 @@ def _render_asdict_stub(model_contexts: Sequence[ModelRenderContext]) -> str:
|
|
|
472
486
|
return code
|
|
473
487
|
|
|
474
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
|
+
|
|
475
507
|
def _build_relation_entries(info: ModelInfo, model_infos: Mapping[str, ModelInfo]) -> list[dict[str, Any]]:
|
|
476
508
|
entries: list[dict[str, Any]] = []
|
|
477
509
|
if not info.relations:
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
[ModelInfo, SchemaPlan, tuple[ExistingColumn, ...] | None, SchemaDiff],
|
|
3
|
-
bool,
|
|
4
|
-
]
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class Client(BaseDBPool):
|
|
1
|
+
class {{ client.class_name }}(BaseDBPool):
|
|
8
2
|
datasource: DataSourceConfig = DataSourceConfig(
|
|
9
3
|
provider={{ client.datasource.provider_repr }},
|
|
10
4
|
url={{ client.datasource.url_repr }},
|
|
@@ -41,7 +35,7 @@ class Client(BaseDBPool):
|
|
|
41
35
|
self,
|
|
42
36
|
*,
|
|
43
37
|
sync_indexes: bool = False,
|
|
44
|
-
|
|
38
|
+
force_rebuild: bool = False,
|
|
45
39
|
) -> None:
|
|
46
40
|
connection = self._open_connection(self.datasource)
|
|
47
41
|
try:
|
|
@@ -51,7 +45,7 @@ class Client(BaseDBPool):
|
|
|
51
45
|
{% endfor %} ),
|
|
52
46
|
{self.datasource_key: connection},
|
|
53
47
|
sync_indexes=sync_indexes,
|
|
54
|
-
confirm_rebuild=
|
|
48
|
+
confirm_rebuild=(lambda *_: True) if force_rebuild else None,
|
|
55
49
|
)
|
|
56
50
|
finally:
|
|
57
51
|
connection.close()
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
from enum import Enum
|
|
5
5
|
from types import MappingProxyType
|
|
6
|
-
from typing import Any,
|
|
6
|
+
from typing import Any, Literal, Mapping, Sequence, NotRequired, overload
|
|
7
7
|
from typing_extensions import TypedDict
|
|
8
8
|
|
|
9
9
|
from dclassql import DataSourceConfig, db_push
|
|
@@ -11,8 +11,6 @@ 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
|
|
14
|
-
from dclassql.push.base import ExistingColumn, SchemaDiff, SchemaPlan
|
|
15
|
-
from dclassql.model_inspector import ModelInfo
|
|
16
14
|
|
|
17
15
|
{% if module_imports %}
|
|
18
16
|
{% for block in module_imports -%}
|
|
@@ -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: ...
|