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.
Files changed (40) hide show
  1. {dclassql-0.3.0 → dclassql-0.4.0}/PKG-INFO +1 -1
  2. {dclassql-0.3.0 → dclassql-0.4.0}/pyproject.toml +1 -1
  3. dclassql-0.4.0/src/dclassql/__init__.py +21 -0
  4. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/cli.py +31 -13
  5. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/codegen.py +55 -47
  6. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/model_inspector.py +1 -1
  7. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/runtime/backends/protocols.py +2 -0
  8. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/runtime/backends/where_compiler.py +9 -5
  9. dclassql-0.4.0/src/dclassql/templates/partials/client_class.jinja +60 -0
  10. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/templates/partials/imports.jinja +2 -2
  11. dclassql-0.3.0/src/dclassql/__init__.py +0 -34
  12. dclassql-0.3.0/src/dclassql/asdict.pyi +0 -57
  13. dclassql-0.3.0/src/dclassql/templates/partials/client_class.jinja +0 -32
  14. {dclassql-0.3.0 → dclassql-0.4.0}/README.md +0 -0
  15. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/.gitignore +0 -0
  16. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/asdict.py +0 -0
  17. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/db_pool.py +0 -0
  18. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/push/__init__.py +0 -0
  19. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/push/base.py +0 -0
  20. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/push/sqlite.py +0 -0
  21. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/runtime/backends/__init__.py +0 -0
  22. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/runtime/backends/base.py +0 -0
  23. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/runtime/backends/lazy.py +0 -0
  24. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/runtime/backends/metadata.py +0 -0
  25. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/runtime/backends/sqlite.py +0 -0
  26. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/runtime/datasource.py +0 -0
  27. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/runtime/sql_recorder.py +0 -0
  28. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/runtime/sqlite_adapters.py +0 -0
  29. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/table_spec.py +0 -0
  30. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/templates/__init__.py +0 -0
  31. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/templates/asdict_stub.pyi.jinja +0 -0
  32. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/templates/client_module.py.jinja +0 -0
  33. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/templates/partials/exports.jinja +0 -0
  34. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/templates/partials/macros.jinja +0 -0
  35. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/templates/partials/model_section.jinja +0 -0
  36. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/templates/partials/scalar_filters.jinja +0 -0
  37. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/typing.py +0 -0
  38. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/unwarp.py +0 -0
  39. {dclassql-0.3.0 → dclassql-0.4.0}/src/dclassql/utils/__init__.py +0 -0
  40. {dclassql-0.3.0 → 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.0
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.0"
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)
@@ -131,7 +134,7 @@ class ModelRenderContext:
131
134
 
132
135
 
133
136
  @dataclass(slots=True)
134
- class ClientDatasourceContext:
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
- datasource_items: tuple[ClientDatasourceContext, ...]
159
- backend_methods: tuple[BackendMethodContext, ...]
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
- 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
+ )
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: dict[str, DataSourceConfig] = {}
423
- for info in model_infos.values():
424
- datasource = info.datasource
425
- key = datasource.name or datasource.provider
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
- for key, ds in sorted(datasource_configs.items())
441
- ]
442
-
443
- backend_methods: list[BackendMethodContext] = []
444
- method_map: dict[str, str] = {}
445
- for key in sorted(datasource_configs.keys()):
446
- method_name = f"_backend_{_sanitize_identifier(key)}"
447
- backend_methods.append(BackendMethodContext(key=key, key_repr=repr(key), method_name=method_name))
448
- method_map[key] = method_name
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
- datasource_items=tuple(datasource_items),
461
- backend_methods=tuple(backend_methods),
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", "Client"]
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:
@@ -48,7 +48,7 @@ class ModelInfo:
48
48
  datasource: 'DataSourceConfig'
49
49
 
50
50
 
51
- @dataclass(slots=True)
51
+ @dataclass(slots=True, frozen=True)
52
52
  class DataSourceConfig:
53
53
  provider: str
54
54
  url: str | None
@@ -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) -> Criterion:
45
- return EscapeLikeCriterion(self._field, self._parameter, escape=self._escape, negated=not self._negated)
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: # type: ignore[override]
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, save_local
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