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.
Files changed (40) hide show
  1. {dclassql-0.3.1 → dclassql-0.4.0}/PKG-INFO +1 -1
  2. {dclassql-0.3.1 → dclassql-0.4.0}/pyproject.toml +1 -1
  3. dclassql-0.4.0/src/dclassql/__init__.py +21 -0
  4. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/cli.py +31 -13
  5. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/codegen.py +39 -7
  6. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/templates/partials/client_class.jinja +3 -9
  7. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/templates/partials/imports.jinja +1 -3
  8. dclassql-0.3.1/src/dclassql/__init__.py +0 -34
  9. dclassql-0.3.1/src/dclassql/asdict.pyi +0 -57
  10. dclassql-0.3.1/src/dclassql/client.py +0 -1397
  11. {dclassql-0.3.1 → dclassql-0.4.0}/README.md +0 -0
  12. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/.gitignore +0 -0
  13. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/asdict.py +0 -0
  14. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/db_pool.py +0 -0
  15. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/model_inspector.py +0 -0
  16. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/push/__init__.py +0 -0
  17. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/push/base.py +0 -0
  18. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/push/sqlite.py +0 -0
  19. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/runtime/backends/__init__.py +0 -0
  20. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/runtime/backends/base.py +0 -0
  21. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/runtime/backends/lazy.py +0 -0
  22. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/runtime/backends/metadata.py +0 -0
  23. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/runtime/backends/protocols.py +0 -0
  24. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/runtime/backends/sqlite.py +0 -0
  25. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/runtime/backends/where_compiler.py +0 -0
  26. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/runtime/datasource.py +0 -0
  27. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/runtime/sql_recorder.py +0 -0
  28. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/runtime/sqlite_adapters.py +0 -0
  29. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/table_spec.py +0 -0
  30. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/templates/__init__.py +0 -0
  31. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/templates/asdict_stub.pyi.jinja +0 -0
  32. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/templates/client_module.py.jinja +0 -0
  33. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/templates/partials/exports.jinja +0 -0
  34. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/templates/partials/macros.jinja +0 -0
  35. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/templates/partials/model_section.jinja +0 -0
  36. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/templates/partials/scalar_filters.jinja +0 -0
  37. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/typing.py +0 -0
  38. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/unwarp.py +0 -0
  39. {dclassql-0.3.1 → dclassql-0.4.0}/src/dclassql/utils/__init__.py +0 -0
  40. {dclassql-0.3.1 → dclassql-0.4.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.1
3
+ Version: 0.4.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dclassql"
3
- version = "0.3.1"
3
+ version = "0.4.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 = [
@@ -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 resolve_generated_path() -> Path:
70
- return _find_package_directory() / GENERATED_CLIENT_FILENAME
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 resolve_asdict_stub_path() -> Path:
74
- return _find_package_directory() / "asdict.pyi"
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
- generated = generate_client(models)
169
- output_path = resolve_generated_path()
170
- output_path.parent.mkdir(parents=True, exist_ok=True)
171
- output_path.write_text(generated.code, encoding="utf-8")
172
- asdict_stub_path = resolve_asdict_stub_path()
173
- asdict_stub_path.write_text(generated.asdict_stub, encoding="utf-8")
174
- sys.stdout.write(f"Client written to {output_path}\n")
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.set_defaults(handler=lambda args: command_generate(args.module))
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
- return GeneratedModule(code=code, asdict_stub=asdict_stub, model_names=tuple(sorted(model_infos.keys())))
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", "Client"]
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
- ConfirmRebuildCallback = Callable[
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
- confirm_rebuild: ConfirmRebuildCallback | None = None,
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=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, Callable, Literal, Mapping, Sequence, NotRequired, overload
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: ...