dclassql 0.1.6__tar.gz → 0.3.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.1.6 → dclassql-0.3.0}/PKG-INFO +2 -2
- {dclassql-0.1.6 → dclassql-0.3.0}/README.md +1 -1
- {dclassql-0.1.6 → dclassql-0.3.0}/pyproject.toml +3 -3
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/__init__.py +7 -4
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/cli.py +0 -46
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/codegen.py +54 -10
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/runtime/backends/base.py +305 -29
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/runtime/backends/protocols.py +70 -5
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/runtime/backends/sqlite.py +66 -0
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/runtime/backends/where_compiler.py +38 -6
- dclassql-0.3.0/src/dclassql/runtime/sql_recorder.py +44 -0
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/runtime/sqlite_adapters.py +0 -3
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/templates/partials/imports.jinja +1 -1
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/templates/partials/model_section.jinja +73 -8
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/typing.py +1 -0
- dclassql-0.1.6/src/dclassql/client.py +0 -803
- dclassql-0.1.6/src/dclassql/generated_models/__init__.py +0 -0
- dclassql-0.1.6/src/dclassql/generated_models/test_models.py +0 -78
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/.gitignore +0 -0
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/asdict.py +0 -0
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/asdict.pyi +0 -0
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/db_pool.py +0 -0
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/model_inspector.py +0 -0
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/push/__init__.py +0 -0
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/push/base.py +0 -0
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/push/sqlite.py +0 -0
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/runtime/backends/__init__.py +0 -0
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/runtime/backends/lazy.py +0 -0
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/runtime/backends/metadata.py +0 -0
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/runtime/datasource.py +0 -0
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/table_spec.py +0 -0
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/templates/__init__.py +0 -0
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/templates/asdict_stub.pyi.jinja +0 -0
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/templates/client_module.py.jinja +0 -0
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/templates/partials/client_class.jinja +0 -0
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/templates/partials/exports.jinja +0 -0
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/templates/partials/macros.jinja +0 -0
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/templates/partials/scalar_filters.jinja +0 -0
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/unwarp.py +0 -0
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/utils/__init__.py +0 -0
- {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/utils/ensure.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: dclassql
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A type-safe ORM generator for Python, creating fully type-hinted database clients from plain dataclass definitions.
|
|
5
5
|
Keywords: orm,codegen,sqlite,dataclass,typed
|
|
6
6
|
Author: myuanz
|
|
@@ -86,7 +86,7 @@ uv add dclassql
|
|
|
86
86
|
|
|
87
87
|
## 当前状态
|
|
88
88
|
|
|
89
|
-
DataclassQL 仍在早期开发阶段,
|
|
89
|
+
DataclassQL 仍在早期开发阶段, 但不是无根浮萍, 我已经在另外两个项目里大量使用, 目前基于其他项目的反馈来更新.
|
|
90
90
|
|
|
91
91
|
## 一份更长的例子
|
|
92
92
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dclassql"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "A type-safe ORM generator for Python, creating fully type-hinted database clients from plain dataclass definitions."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -26,10 +26,10 @@ dependencies = [
|
|
|
26
26
|
]
|
|
27
27
|
|
|
28
28
|
[project.scripts]
|
|
29
|
-
"
|
|
29
|
+
"dclassql" = "dclassql.cli:main"
|
|
30
30
|
|
|
31
31
|
[build-system]
|
|
32
|
-
requires = ["uv_build>=0.
|
|
32
|
+
requires = ["uv_build>=0.11.6,<0.12.0"]
|
|
33
33
|
build-backend = "uv_build"
|
|
34
34
|
|
|
35
35
|
[dependency-groups]
|
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
from .
|
|
1
|
+
from .asdict import asdict
|
|
2
2
|
from .db_pool import BaseDBPool, save_local
|
|
3
|
+
from .model_inspector import DataSourceConfig
|
|
3
4
|
from .push import db_push
|
|
4
5
|
from .runtime.backends.lazy import eager
|
|
5
|
-
from .
|
|
6
|
+
from .runtime.sql_recorder import record_sql
|
|
6
7
|
from .unwarp import unwarp, unwarp_or, unwarp_or_raise
|
|
7
8
|
|
|
9
|
+
|
|
8
10
|
class _MissingClient:
|
|
9
11
|
def __init__(self, *args: object, **kwargs: object) -> None:
|
|
10
12
|
raise RuntimeError(
|
|
11
|
-
"dclassql.Client 尚未生成。请先运行 `
|
|
13
|
+
"dclassql.Client 尚未生成。请先运行 `dclassql -m <model.py> generate` 生成客户端后再导入。"
|
|
12
14
|
)
|
|
13
15
|
|
|
14
16
|
try: # pragma: no cover - exercised in integration tests
|
|
15
17
|
from .client import Client # type: ignore
|
|
16
|
-
except ModuleNotFoundError: # pragma: no cover - fallback when未生成
|
|
18
|
+
except (ModuleNotFoundError, ImportError): # pragma: no cover - fallback when未生成
|
|
17
19
|
Client = _MissingClient # type: ignore[assignment]
|
|
18
20
|
|
|
19
21
|
|
|
@@ -28,4 +30,5 @@ __all__ = [
|
|
|
28
30
|
'BaseDBPool',
|
|
29
31
|
'save_local',
|
|
30
32
|
'DataSourceConfig',
|
|
33
|
+
'record_sql',
|
|
31
34
|
]
|
|
@@ -3,9 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import argparse
|
|
4
4
|
import importlib.util
|
|
5
5
|
import re
|
|
6
|
-
import shutil
|
|
7
6
|
import sys
|
|
8
|
-
import warnings
|
|
9
7
|
from pathlib import Path
|
|
10
8
|
from types import ModuleType
|
|
11
9
|
from typing import Any, Callable, Literal, Sequence
|
|
@@ -19,7 +17,6 @@ from .runtime.datasource import open_sqlite_connection
|
|
|
19
17
|
|
|
20
18
|
DEFAULT_MODEL_FILE = "model.py"
|
|
21
19
|
GENERATED_CLIENT_FILENAME = "client.py"
|
|
22
|
-
GENERATED_MODELS_DIRNAME = "generated_models"
|
|
23
20
|
|
|
24
21
|
ConfirmRebuildMode = Literal["auto", "prompt"]
|
|
25
22
|
ConfirmCallback = Callable[
|
|
@@ -77,43 +74,6 @@ def resolve_asdict_stub_path() -> Path:
|
|
|
77
74
|
return _find_package_directory() / "asdict.pyi"
|
|
78
75
|
|
|
79
76
|
|
|
80
|
-
def resolve_models_directory() -> Path:
|
|
81
|
-
return _find_package_directory() / GENERATED_MODELS_DIRNAME
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def _sanitize_name(name: str) -> str:
|
|
85
|
-
sanitized = re.sub(r"[^0-9a-zA-Z_]+", "_", name).strip("_")
|
|
86
|
-
return sanitized.lower() or "models"
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def compute_model_target(module_path: Path) -> tuple[Path, str]:
|
|
90
|
-
base_dir = resolve_models_directory()
|
|
91
|
-
base_dir.mkdir(parents=True, exist_ok=True)
|
|
92
|
-
(base_dir / "__init__.py").touch()
|
|
93
|
-
|
|
94
|
-
module_name = _sanitize_name(module_path.stem)
|
|
95
|
-
|
|
96
|
-
target_path = base_dir / f"{module_name}.py"
|
|
97
|
-
import_path = f"dclassql.{GENERATED_MODELS_DIRNAME}.{module_name}"
|
|
98
|
-
return target_path, import_path
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def materialize_model_module(module_path: Path) -> str:
|
|
102
|
-
target_path, import_path = compute_model_target(module_path)
|
|
103
|
-
if target_path.exists() or target_path.is_symlink():
|
|
104
|
-
target_path.unlink()
|
|
105
|
-
try:
|
|
106
|
-
target_path.symlink_to(module_path.resolve())
|
|
107
|
-
except OSError as exc:
|
|
108
|
-
warnings.warn(
|
|
109
|
-
f"Unable to create symlink for model '{module_path}'; falling back to copy. ({exc})",
|
|
110
|
-
RuntimeWarning,
|
|
111
|
-
stacklevel=2,
|
|
112
|
-
)
|
|
113
|
-
shutil.copy2(module_path, target_path)
|
|
114
|
-
return import_path
|
|
115
|
-
|
|
116
|
-
|
|
117
77
|
def collect_models(module: ModuleType) -> list[type[Any]]:
|
|
118
78
|
from dataclasses import is_dataclass
|
|
119
79
|
|
|
@@ -205,13 +165,7 @@ def push_database(
|
|
|
205
165
|
def command_generate(module_path: Path) -> None:
|
|
206
166
|
module = load_module(module_path)
|
|
207
167
|
models = collect_models(module)
|
|
208
|
-
generated_models_module = materialize_model_module(module_path)
|
|
209
|
-
original_modules = {model: model.__module__ for model in models}
|
|
210
|
-
for model in models:
|
|
211
|
-
model.__module__ = generated_models_module
|
|
212
168
|
generated = generate_client(models)
|
|
213
|
-
for model, original in original_modules.items():
|
|
214
|
-
model.__module__ = original
|
|
215
169
|
output_path = resolve_generated_path()
|
|
216
170
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
217
171
|
output_path.write_text(generated.code, encoding="utf-8")
|
|
@@ -100,6 +100,12 @@ class ScalarFilterRender:
|
|
|
100
100
|
fields: tuple[TypedDictFieldSpec, ...]
|
|
101
101
|
|
|
102
102
|
|
|
103
|
+
@dataclass(slots=True)
|
|
104
|
+
class UpsertWhereRender:
|
|
105
|
+
name: str
|
|
106
|
+
fields: tuple[TypedDictFieldSpec, ...]
|
|
107
|
+
|
|
108
|
+
|
|
103
109
|
@dataclass(slots=True)
|
|
104
110
|
class ModelRenderContext:
|
|
105
111
|
name: str
|
|
@@ -107,6 +113,8 @@ class ModelRenderContext:
|
|
|
107
113
|
table_name_literal: str
|
|
108
114
|
insert_fields: tuple[InsertFieldSpec, ...]
|
|
109
115
|
typed_dict_fields: tuple[TypedDictFieldSpec, ...]
|
|
116
|
+
update_fields: tuple[TypedDictFieldSpec, ...]
|
|
117
|
+
upsert_where_dicts: tuple["UpsertWhereRender", ...]
|
|
110
118
|
dict_fields: tuple[TypedDictFieldSpec, ...]
|
|
111
119
|
where_fields: tuple[WhereFieldSpec, ...]
|
|
112
120
|
relation_filters: tuple[RelationFilterRender, ...]
|
|
@@ -116,6 +124,7 @@ class ModelRenderContext:
|
|
|
116
124
|
primary_key_literal: str
|
|
117
125
|
indexes_literal: str
|
|
118
126
|
unique_indexes_literal: str
|
|
127
|
+
primary_value_types: tuple[str, ...]
|
|
119
128
|
row_assignments: tuple[RowAssignmentRender, ...]
|
|
120
129
|
default_factories: tuple[DefaultFactoryRender, ...]
|
|
121
130
|
model_info: ModelInfo
|
|
@@ -172,21 +181,19 @@ def generate_client(models: Sequence[type[Any]]) -> GeneratedModule:
|
|
|
172
181
|
renderer = _TypeRenderer({info.model: name for name, info in model_infos.items()})
|
|
173
182
|
filter_registry = _ScalarFilterRegistry(renderer)
|
|
174
183
|
|
|
175
|
-
model_imports:
|
|
184
|
+
model_imports: defaultdict[str, set[str]] = defaultdict(set)
|
|
176
185
|
for info in model_infos.values():
|
|
177
|
-
|
|
178
|
-
model_imports.setdefault(module, set()).add(info.model.__name__)
|
|
186
|
+
model_imports[info.model.__module__].add(info.model.__name__)
|
|
179
187
|
|
|
180
188
|
model_contexts = [
|
|
181
189
|
_build_model_context(model_infos[name], renderer, model_infos, filter_registry)
|
|
182
190
|
for name in sorted(model_infos.keys())
|
|
183
191
|
]
|
|
184
192
|
|
|
185
|
-
|
|
186
|
-
combined_imports: dict[str, set[str]] = defaultdict(set)
|
|
193
|
+
combined_imports: defaultdict[str, set[str]] = defaultdict(set)
|
|
187
194
|
for module, names in model_imports.items():
|
|
188
195
|
combined_imports[module].update(names)
|
|
189
|
-
for module, names in module_imports.items():
|
|
196
|
+
for module, names in renderer.module_imports.items():
|
|
190
197
|
combined_imports[module].update(names)
|
|
191
198
|
|
|
192
199
|
import_blocks = [
|
|
@@ -222,8 +229,11 @@ def _build_model_context(
|
|
|
222
229
|
|
|
223
230
|
insert_fields: list[InsertFieldSpec] = []
|
|
224
231
|
typed_dict_fields: list[TypedDictFieldSpec] = []
|
|
232
|
+
update_fields: list[TypedDictFieldSpec] = []
|
|
233
|
+
upsert_where_dicts: list[UpsertWhereRender] = []
|
|
225
234
|
dict_field_map: dict[str, str] = {}
|
|
226
235
|
enum_type_map: dict[str, type[Enum] | None] = {}
|
|
236
|
+
column_lookup: dict[str, ColumnInfo] = {col.name: col for col in info.columns}
|
|
227
237
|
for col in info.columns:
|
|
228
238
|
annotation = _format_insert_annotation(col, renderer)
|
|
229
239
|
default_fragment = _render_default_fragment(name, col)
|
|
@@ -248,6 +258,27 @@ def _build_model_context(
|
|
|
248
258
|
enum_type = _resolve_enum_class(col.python_type)
|
|
249
259
|
enum_type_map[col.name] = enum_type
|
|
250
260
|
|
|
261
|
+
update_fields.append(TypedDictFieldSpec(name=col.name, annotation=renderer.render(col.python_type)))
|
|
262
|
+
|
|
263
|
+
if info.primary_key:
|
|
264
|
+
pk_fields: list[TypedDictFieldSpec] = []
|
|
265
|
+
for pk_col in info.primary_key:
|
|
266
|
+
col_info = column_lookup.get(pk_col)
|
|
267
|
+
annotation = renderer.render(col_info.python_type) if col_info else "object"
|
|
268
|
+
pk_fields.append(TypedDictFieldSpec(name=pk_col, annotation=annotation))
|
|
269
|
+
upsert_where_dicts.append(UpsertWhereRender(name=f"{name}UpsertWherePK", fields=tuple(pk_fields)))
|
|
270
|
+
|
|
271
|
+
if info.unique_indexes:
|
|
272
|
+
for idx, unique_cols in enumerate(info.unique_indexes, start=1):
|
|
273
|
+
unique_fields: list[TypedDictFieldSpec] = []
|
|
274
|
+
for col_name in unique_cols:
|
|
275
|
+
col_info = column_lookup.get(col_name)
|
|
276
|
+
annotation = renderer.render(col_info.python_type) if col_info else "object"
|
|
277
|
+
unique_fields.append(TypedDictFieldSpec(name=col_name, annotation=annotation))
|
|
278
|
+
upsert_where_dicts.append(
|
|
279
|
+
UpsertWhereRender(name=f"{name}UpsertWhereUnique{idx}", fields=tuple(unique_fields))
|
|
280
|
+
)
|
|
281
|
+
|
|
251
282
|
where_fields: list[WhereFieldSpec] = []
|
|
252
283
|
for col in info.columns:
|
|
253
284
|
annotation = renderer.render(col.python_type)
|
|
@@ -334,6 +365,13 @@ def _build_model_context(
|
|
|
334
365
|
)
|
|
335
366
|
|
|
336
367
|
row_assignments, default_factories = _build_row_assignment_context(info, enum_type_map)
|
|
368
|
+
primary_value_types: list[str] = []
|
|
369
|
+
for column_name in info.primary_key:
|
|
370
|
+
column = column_lookup.get(column_name)
|
|
371
|
+
if column is None:
|
|
372
|
+
primary_value_types.append("object")
|
|
373
|
+
continue
|
|
374
|
+
primary_value_types.append(renderer.render(column.python_type))
|
|
337
375
|
|
|
338
376
|
relation_lookup = {relation.name: relation for relation in info.relations}
|
|
339
377
|
dataclass_fields = fields(info.model)
|
|
@@ -362,6 +400,8 @@ def _build_model_context(
|
|
|
362
400
|
table_name_literal=repr(name),
|
|
363
401
|
insert_fields=tuple(insert_fields),
|
|
364
402
|
typed_dict_fields=tuple(typed_dict_fields),
|
|
403
|
+
update_fields=tuple(update_fields),
|
|
404
|
+
upsert_where_dicts=tuple(upsert_where_dicts),
|
|
365
405
|
dict_fields=tuple(dict_fields),
|
|
366
406
|
where_fields=tuple(where_fields),
|
|
367
407
|
relation_filters=tuple(relation_filters),
|
|
@@ -371,6 +411,7 @@ def _build_model_context(
|
|
|
371
411
|
primary_key_literal=_tuple_literal(info.primary_key),
|
|
372
412
|
indexes_literal=indexes_literal,
|
|
373
413
|
unique_indexes_literal=unique_indexes_literal,
|
|
414
|
+
primary_value_types=tuple(primary_value_types),
|
|
374
415
|
row_assignments=tuple(row_assignments),
|
|
375
416
|
default_factories=tuple(default_factories),
|
|
376
417
|
model_info=info,
|
|
@@ -436,6 +477,8 @@ def _collect_exports(model_contexts: Sequence[ModelRenderContext]) -> list[str]:
|
|
|
436
477
|
f"{name}Dict",
|
|
437
478
|
f"{name}Insert",
|
|
438
479
|
f"{name}InsertDict",
|
|
480
|
+
f"{name}UpdateDict",
|
|
481
|
+
f"{name}UpsertWhereDict",
|
|
439
482
|
f"{name}WhereDict",
|
|
440
483
|
f"{name}Table",
|
|
441
484
|
]
|
|
@@ -852,7 +895,7 @@ class _ScalarFilterRegistry:
|
|
|
852
895
|
class _TypeRenderer:
|
|
853
896
|
def __init__(self, model_map: Mapping[type[Any], str]) -> None:
|
|
854
897
|
self._model_map = dict(model_map)
|
|
855
|
-
self._module_imports:
|
|
898
|
+
self._module_imports: defaultdict[str, set[str]] = defaultdict(set) # {module: set of names}
|
|
856
899
|
self._typing_imports: set[str] = set()
|
|
857
900
|
|
|
858
901
|
def render(self, tp: Any) -> str:
|
|
@@ -896,13 +939,14 @@ class _TypeRenderer:
|
|
|
896
939
|
if tp.__module__ == "builtins":
|
|
897
940
|
return tp.__name__
|
|
898
941
|
if tp.__module__ == "datetime":
|
|
899
|
-
self._module_imports
|
|
942
|
+
self._module_imports["datetime"].add(tp.__name__)
|
|
900
943
|
return tp.__name__
|
|
901
|
-
self._module_imports
|
|
944
|
+
self._module_imports[tp.__module__].add(tp.__qualname__.split(".")[0])
|
|
902
945
|
return tp.__qualname__
|
|
903
946
|
return repr(tp)
|
|
904
947
|
|
|
905
|
-
|
|
948
|
+
@property
|
|
949
|
+
def module_imports(self) -> defaultdict[str, set[str]]:
|
|
906
950
|
return self._module_imports
|
|
907
951
|
|
|
908
952
|
@property
|