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.
Files changed (41) hide show
  1. {dclassql-0.1.6 → dclassql-0.3.0}/PKG-INFO +2 -2
  2. {dclassql-0.1.6 → dclassql-0.3.0}/README.md +1 -1
  3. {dclassql-0.1.6 → dclassql-0.3.0}/pyproject.toml +3 -3
  4. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/__init__.py +7 -4
  5. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/cli.py +0 -46
  6. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/codegen.py +54 -10
  7. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/runtime/backends/base.py +305 -29
  8. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/runtime/backends/protocols.py +70 -5
  9. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/runtime/backends/sqlite.py +66 -0
  10. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/runtime/backends/where_compiler.py +38 -6
  11. dclassql-0.3.0/src/dclassql/runtime/sql_recorder.py +44 -0
  12. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/runtime/sqlite_adapters.py +0 -3
  13. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/templates/partials/imports.jinja +1 -1
  14. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/templates/partials/model_section.jinja +73 -8
  15. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/typing.py +1 -0
  16. dclassql-0.1.6/src/dclassql/client.py +0 -803
  17. dclassql-0.1.6/src/dclassql/generated_models/__init__.py +0 -0
  18. dclassql-0.1.6/src/dclassql/generated_models/test_models.py +0 -78
  19. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/.gitignore +0 -0
  20. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/asdict.py +0 -0
  21. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/asdict.pyi +0 -0
  22. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/db_pool.py +0 -0
  23. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/model_inspector.py +0 -0
  24. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/push/__init__.py +0 -0
  25. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/push/base.py +0 -0
  26. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/push/sqlite.py +0 -0
  27. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/runtime/backends/__init__.py +0 -0
  28. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/runtime/backends/lazy.py +0 -0
  29. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/runtime/backends/metadata.py +0 -0
  30. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/runtime/datasource.py +0 -0
  31. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/table_spec.py +0 -0
  32. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/templates/__init__.py +0 -0
  33. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/templates/asdict_stub.pyi.jinja +0 -0
  34. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/templates/client_module.py.jinja +0 -0
  35. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/templates/partials/client_class.jinja +0 -0
  36. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/templates/partials/exports.jinja +0 -0
  37. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/templates/partials/macros.jinja +0 -0
  38. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/templates/partials/scalar_filters.jinja +0 -0
  39. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/unwarp.py +0 -0
  40. {dclassql-0.1.6 → dclassql-0.3.0}/src/dclassql/utils/__init__.py +0 -0
  41. {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.1.6
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 仍在早期开发阶段, 已完成代码生成和 SQLite 支持, 后续将扩展更多数据库与查询功能.
89
+ DataclassQL 仍在早期开发阶段, 但不是无根浮萍, 我已经在另外两个项目里大量使用, 目前基于其他项目的反馈来更新.
90
90
 
91
91
  ## 一份更长的例子
92
92
 
@@ -65,7 +65,7 @@ uv add dclassql
65
65
 
66
66
  ## 当前状态
67
67
 
68
- DataclassQL 仍在早期开发阶段, 已完成代码生成和 SQLite 支持, 后续将扩展更多数据库与查询功能.
68
+ DataclassQL 仍在早期开发阶段, 但不是无根浮萍, 我已经在另外两个项目里大量使用, 目前基于其他项目的反馈来更新.
69
69
 
70
70
  ## 一份更长的例子
71
71
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dclassql"
3
- version = "0.1.6"
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
- "dql" = "dclassql.cli:main"
29
+ "dclassql" = "dclassql.cli:main"
30
30
 
31
31
  [build-system]
32
- requires = ["uv_build>=0.8.22,<0.9.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 .model_inspector import DataSourceConfig
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 .asdict import asdict
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 尚未生成。请先运行 `dql -m <model.py> generate` 生成客户端后再导入。"
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: dict[str, set[str]] = defaultdict(set)
184
+ model_imports: defaultdict[str, set[str]] = defaultdict(set)
176
185
  for info in model_infos.values():
177
- module = info.model.__module__
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
- module_imports = renderer.build_imports()
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: dict[str, set[str]] = defaultdict(set)
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.setdefault("datetime", set()).add(tp.__name__)
942
+ self._module_imports["datetime"].add(tp.__name__)
900
943
  return tp.__name__
901
- self._module_imports.setdefault(tp.__module__, set()).add(tp.__qualname__.split(".")[0])
944
+ self._module_imports[tp.__module__].add(tp.__qualname__.split(".")[0])
902
945
  return tp.__qualname__
903
946
  return repr(tp)
904
947
 
905
- def build_imports(self) -> Mapping[str, set[str]]:
948
+ @property
949
+ def module_imports(self) -> defaultdict[str, set[str]]:
906
950
  return self._module_imports
907
951
 
908
952
  @property