dclassql 0.1.0__tar.gz → 0.1.2__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 (38) hide show
  1. {dclassql-0.1.0 → dclassql-0.1.2}/PKG-INFO +13 -2
  2. dclassql-0.1.2/pyproject.toml +39 -0
  3. dclassql-0.1.2/src/dclassql/.gitignore +2 -0
  4. dclassql-0.1.2/src/dclassql/__init__.py +27 -0
  5. {dclassql-0.1.0 → dclassql-0.1.2}/src/dclassql/cli.py +64 -2
  6. dclassql-0.1.2/src/dclassql/client.py +496 -0
  7. dclassql-0.1.2/src/dclassql/codegen.py +507 -0
  8. dclassql-0.1.2/src/dclassql/generated_models/__init__.py +0 -0
  9. dclassql-0.1.2/src/dclassql/generated_models/test_models.py +78 -0
  10. dclassql-0.1.2/src/dclassql/runtime/backends/__init__.py +15 -0
  11. dclassql-0.1.2/src/dclassql/runtime/backends/base.py +323 -0
  12. dclassql-0.1.2/src/dclassql/runtime/backends/lazy.py +376 -0
  13. dclassql-0.1.2/src/dclassql/runtime/backends/metadata.py +31 -0
  14. dclassql-0.1.2/src/dclassql/runtime/backends/protocols.py +66 -0
  15. dclassql-0.1.2/src/dclassql/runtime/backends/sqlite.py +128 -0
  16. {dclassql-0.1.0 → dclassql-0.1.2}/src/dclassql/runtime/datasource.py +7 -1
  17. dclassql-0.1.2/src/dclassql/runtime/sqlite_adapters.py +43 -0
  18. dclassql-0.1.2/src/dclassql/templates/__init__.py +1 -0
  19. dclassql-0.1.2/src/dclassql/templates/client_module.py.jinja +12 -0
  20. dclassql-0.1.2/src/dclassql/templates/partials/client_class.jinja +29 -0
  21. dclassql-0.1.2/src/dclassql/templates/partials/datasource_config.jinja +11 -0
  22. dclassql-0.1.2/src/dclassql/templates/partials/exports.jinja +3 -0
  23. dclassql-0.1.2/src/dclassql/templates/partials/imports.jinja +17 -0
  24. dclassql-0.1.2/src/dclassql/templates/partials/macros.jinja +82 -0
  25. dclassql-0.1.2/src/dclassql/templates/partials/model_section.jinja +85 -0
  26. dclassql-0.1.2/src/dclassql/typing.py +7 -0
  27. dclassql-0.1.0/pyproject.toml +0 -25
  28. dclassql-0.1.0/src/dclassql/__init__.py +0 -12
  29. dclassql-0.1.0/src/dclassql/codegen.py +0 -508
  30. dclassql-0.1.0/src/dclassql/runtime/backends.py +0 -669
  31. {dclassql-0.1.0 → dclassql-0.1.2}/README.md +0 -0
  32. {dclassql-0.1.0 → dclassql-0.1.2}/src/dclassql/db_pool.py +0 -0
  33. {dclassql-0.1.0 → dclassql-0.1.2}/src/dclassql/model_inspector.py +0 -0
  34. {dclassql-0.1.0 → dclassql-0.1.2}/src/dclassql/push/__init__.py +0 -0
  35. {dclassql-0.1.0 → dclassql-0.1.2}/src/dclassql/push/base.py +0 -0
  36. {dclassql-0.1.0 → dclassql-0.1.2}/src/dclassql/push/sqlite.py +0 -0
  37. {dclassql-0.1.0 → dclassql-0.1.2}/src/dclassql/table_spec.py +0 -0
  38. {dclassql-0.1.0 → dclassql-0.1.2}/src/dclassql/unwarp.py +0 -0
@@ -1,11 +1,22 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dclassql
3
- Version: 0.1.0
4
- Summary: Add your description here
3
+ Version: 0.1.2
4
+ Summary: A type-safe ORM generator for Python, creating fully type-hinted database clients from plain dataclass definitions.
5
+ Keywords: orm,codegen,sqlite,dataclass,typed
5
6
  Author: myuanz
6
7
  Author-email: myuanz <provefars@gmail.com>
8
+ License: MIT
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Programming Language :: Python :: 3.14
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Typing :: Typed
15
+ Requires-Dist: jinja2>=3.1.6
7
16
  Requires-Dist: pypika>=0.48.9
17
+ Requires-Dist: typing-extensions>=4.12.2
8
18
  Requires-Python: >=3.12
19
+ Project-URL: Homepage, https://github.com/myuanz/dataclassql
9
20
  Description-Content-Type: text/markdown
10
21
 
11
22
  # DataclassQL
@@ -0,0 +1,39 @@
1
+ [project]
2
+ name = "dclassql"
3
+ version = "0.1.2"
4
+ description = "A type-safe ORM generator for Python, creating fully type-hinted database clients from plain dataclass definitions."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "myuanz", email = "provefars@gmail.com" }
8
+ ]
9
+ license = { text = "MIT" }
10
+ keywords = ["orm", "codegen", "sqlite", "dataclass", "typed"]
11
+ classifiers = [
12
+ "Programming Language :: Python :: 3.12",
13
+ "Programming Language :: Python :: 3.13",
14
+ "Programming Language :: Python :: 3.14",
15
+
16
+ "License :: OSI Approved :: MIT License",
17
+ "Intended Audience :: Developers",
18
+ "Typing :: Typed",
19
+ ]
20
+ urls = { "Homepage" = "https://github.com/myuanz/dataclassql" }
21
+ requires-python = ">=3.12"
22
+ dependencies = [
23
+ "jinja2>=3.1.6",
24
+ "pypika>=0.48.9",
25
+ "typing_extensions>=4.12.2",
26
+ ]
27
+
28
+ [project.scripts]
29
+ "dql" = "dclassql.cli:main"
30
+
31
+ [build-system]
32
+ requires = ["uv_build>=0.8.22,<0.9.0"]
33
+ build-backend = "uv_build"
34
+
35
+ [dependency-groups]
36
+ dev = [
37
+ "pyright>=1.1.407",
38
+ "pytest>=8.4.2",
39
+ ]
@@ -0,0 +1,2 @@
1
+ client.py
2
+ generated_models/
@@ -0,0 +1,27 @@
1
+ from .db_pool import BaseDBPool, save_local
2
+ from .push import db_push
3
+ from .runtime.backends.lazy import eager
4
+ from .unwarp import unwarp, unwarp_or, unwarp_or_raise
5
+
6
+ class _MissingClient:
7
+ def __init__(self, *args: object, **kwargs: object) -> None:
8
+ raise RuntimeError(
9
+ "dclassql.Client 尚未生成。请先运行 `dql -m <model.py> generate` 生成客户端后再导入。"
10
+ )
11
+
12
+ try: # pragma: no cover - exercised in integration tests
13
+ from .client import Client # type: ignore
14
+ except ModuleNotFoundError: # pragma: no cover - fallback when未生成
15
+ Client = _MissingClient # type: ignore[assignment]
16
+
17
+
18
+ __all__ = [
19
+ 'Client',
20
+ 'db_push',
21
+ 'eager',
22
+ 'unwarp',
23
+ 'unwarp_or',
24
+ 'unwarp_or_raise',
25
+ 'BaseDBPool',
26
+ 'save_local',
27
+ ]
@@ -2,17 +2,22 @@ from __future__ import annotations
2
2
 
3
3
  import argparse
4
4
  import importlib.util
5
+ import re
6
+ import shutil
5
7
  import sys
8
+ import warnings
6
9
  from pathlib import Path
7
10
  from types import ModuleType
8
11
  from typing import Any, Sequence
9
12
 
10
13
  from .codegen import generate_client
11
14
  from .push import db_push
12
- from .runtime.datasource import open_sqlite_connection, resolve_sqlite_path
15
+ from .runtime.datasource import open_sqlite_connection
13
16
 
14
17
 
15
18
  DEFAULT_MODEL_FILE = "model.py"
19
+ GENERATED_CLIENT_FILENAME = "client.py"
20
+ GENERATED_MODELS_DIRNAME = "generated_models"
16
21
 
17
22
 
18
23
  def load_module(module_path: Path) -> ModuleType:
@@ -33,6 +38,54 @@ def load_module(module_path: Path) -> ModuleType:
33
38
  return module
34
39
 
35
40
 
41
+ def _find_package_directory() -> Path:
42
+ spec = importlib.util.find_spec("dclassql")
43
+ if spec is None or not spec.submodule_search_locations:
44
+ raise RuntimeError("Cannot locate installed dclassql package to write generated client")
45
+ return Path(next(iter(spec.submodule_search_locations))).resolve()
46
+
47
+
48
+ def resolve_generated_path() -> Path:
49
+ return _find_package_directory() / GENERATED_CLIENT_FILENAME
50
+
51
+
52
+ def resolve_models_directory() -> Path:
53
+ return _find_package_directory() / GENERATED_MODELS_DIRNAME
54
+
55
+
56
+ def _sanitize_name(name: str) -> str:
57
+ sanitized = re.sub(r"[^0-9a-zA-Z_]+", "_", name).strip("_")
58
+ return sanitized.lower() or "models"
59
+
60
+
61
+ def compute_model_target(module_path: Path) -> tuple[Path, str]:
62
+ base_dir = resolve_models_directory()
63
+ base_dir.mkdir(parents=True, exist_ok=True)
64
+ (base_dir / "__init__.py").touch()
65
+
66
+ module_name = _sanitize_name(module_path.stem)
67
+
68
+ target_path = base_dir / f"{module_name}.py"
69
+ import_path = f"dclassql.{GENERATED_MODELS_DIRNAME}.{module_name}"
70
+ return target_path, import_path
71
+
72
+
73
+ def materialize_model_module(module_path: Path) -> str:
74
+ target_path, import_path = compute_model_target(module_path)
75
+ if target_path.exists() or target_path.is_symlink():
76
+ target_path.unlink()
77
+ try:
78
+ target_path.symlink_to(module_path.resolve())
79
+ except OSError as exc:
80
+ warnings.warn(
81
+ f"Unable to create symlink for model '{module_path}'; falling back to copy. ({exc})",
82
+ RuntimeWarning,
83
+ stacklevel=2,
84
+ )
85
+ shutil.copy2(module_path, target_path)
86
+ return import_path
87
+
88
+
36
89
  def collect_models(module: ModuleType) -> list[type[Any]]:
37
90
  from dataclasses import is_dataclass
38
91
 
@@ -74,8 +127,17 @@ def push_database(models: Sequence[type[Any]]) -> None:
74
127
  def command_generate(module_path: Path) -> None:
75
128
  module = load_module(module_path)
76
129
  models = collect_models(module)
130
+ generated_models_module = materialize_model_module(module_path)
131
+ original_modules = {model: model.__module__ for model in models}
132
+ for model in models:
133
+ model.__module__ = generated_models_module
77
134
  generated = generate_client(models)
78
- sys.stdout.write(generated.code)
135
+ for model, original in original_modules.items():
136
+ model.__module__ = original
137
+ output_path = resolve_generated_path()
138
+ output_path.parent.mkdir(parents=True, exist_ok=True)
139
+ output_path.write_text(generated.code, encoding="utf-8")
140
+ sys.stdout.write(f"Client written to {output_path}\n")
79
141
 
80
142
 
81
143
  def command_push_db(module_path: Path) -> None:
@@ -0,0 +1,496 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from types import MappingProxyType
5
+ from typing import Any, Literal, Mapping, Sequence, NotRequired
6
+ from typing_extensions import TypedDict
7
+
8
+ from dclassql.db_pool import BaseDBPool, save_local
9
+ from dclassql.runtime.backends import BackendProtocol, ColumnSpec, ForeignKeySpec, RelationSpec
10
+ from dclassql.runtime.datasource import open_sqlite_connection
11
+
12
+ from datetime import datetime
13
+ from dclassql.generated_models.test_models import Address, BirthDay, Book, User, UserBook
14
+
15
+ @dataclass(slots=True)
16
+ class DataSourceConfig:
17
+ provider: str
18
+ url: str | None
19
+ name: str | None = None
20
+
21
+ @property
22
+ def key(self) -> str:
23
+ return self.name or self.provider
24
+
25
+
26
+
27
+ TAddressIncludeCol = Literal['user']
28
+ TAddressSortableCol = Literal['id', 'location', 'user_id']
29
+
30
+ @dataclass(slots=True, kw_only=True)
31
+ class AddressInsert:
32
+ id: int | None = None
33
+ location: str
34
+ user_id: int
35
+
36
+
37
+ class AddressInsertDict(TypedDict, closed=True):
38
+ id: NotRequired[int]
39
+ location: str
40
+ user_id: int
41
+
42
+
43
+ class AddressWhereDict(TypedDict, total=False, closed=True):
44
+ id: int | None
45
+ location: str | None
46
+ user_id: int | None
47
+
48
+
49
+ class AddressIncludeDict(TypedDict, total=False, closed=True):
50
+ user: bool
51
+
52
+ class AddressOrderByDict(TypedDict, total=False, closed=True):
53
+ id: Literal['asc', 'desc']
54
+ location: Literal['asc', 'desc']
55
+ user_id: Literal['asc', 'desc']
56
+
57
+ class AddressTable:
58
+ model = Address
59
+ insert_model = AddressInsert
60
+ datasource = DataSourceConfig(provider='sqlite', url='sqlite:///test.db', name=None)
61
+ column_specs: tuple[ColumnSpec, ...] = (
62
+ ColumnSpec(name='id', optional=False, auto_increment=True, has_default=False, has_default_factory=False),
63
+ ColumnSpec(name='location', optional=False, auto_increment=False, has_default=False, has_default_factory=False),
64
+ ColumnSpec(name='user_id', optional=False, auto_increment=False, has_default=False, has_default_factory=False),
65
+ )
66
+ column_specs_by_name: Mapping[str, ColumnSpec] = MappingProxyType({spec.name: spec for spec in column_specs})
67
+ primary_key: tuple[str, ...] = ('id',)
68
+ indexes: tuple[tuple[str, ...], ...] = ()
69
+ unique_indexes: tuple[tuple[str, ...], ...] = ()
70
+ foreign_keys: tuple[ForeignKeySpec, ...] = (
71
+ ForeignKeySpec(
72
+ local_columns=('user_id',),
73
+ remote_model=User,
74
+ remote_columns=('id',),
75
+ backref='addresses',
76
+ ),
77
+ )
78
+
79
+ relations: tuple[RelationSpec, ...] = (
80
+ RelationSpec(name='user', table_name='UserTable', table_module=__name__, many=False, mapping=(('user_id', 'id'),), table_factory=lambda: UserTable),
81
+ )
82
+
83
+
84
+ def __init__(self, backend: BackendProtocol) -> None:
85
+ self._backend = backend
86
+
87
+ def insert(self, data: AddressInsert | AddressInsertDict) -> Address:
88
+ return self._backend.insert(self, data)
89
+
90
+ def insert_many(self, data: Sequence[AddressInsert | AddressInsertDict], *, batch_size: int | None = None) -> list[Address]:
91
+ return self._backend.insert_many(self, data, batch_size=batch_size)
92
+
93
+ def find_many(self, *, where: AddressWhereDict | None = None, include: AddressIncludeDict | None = None, order_by: AddressOrderByDict | None = None, take: int | None = None, skip: int | None = None) -> list[Address]:
94
+ return self._backend.find_many(
95
+ self,
96
+ where=where, include=include, order_by=order_by,
97
+ take=take, skip=skip
98
+ )
99
+
100
+ def find_first(self, *, where: AddressWhereDict | None = None, include: AddressIncludeDict | None = None, order_by: AddressOrderByDict | None = None, skip: int | None = None) -> Address | None:
101
+ return self._backend.find_first(
102
+ self,
103
+ where=where, include=include, order_by=order_by,
104
+ skip=skip
105
+ )
106
+
107
+ TBirthDayIncludeCol = Literal['user']
108
+ TBirthDaySortableCol = Literal['user_id', 'date']
109
+
110
+ @dataclass(slots=True, kw_only=True)
111
+ class BirthDayInsert:
112
+ user_id: int
113
+ date: datetime
114
+
115
+
116
+ class BirthDayInsertDict(TypedDict, closed=True):
117
+ user_id: int
118
+ date: datetime
119
+
120
+
121
+ class BirthDayWhereDict(TypedDict, total=False, closed=True):
122
+ user_id: int | None
123
+ date: datetime | None
124
+
125
+
126
+ class BirthDayIncludeDict(TypedDict, total=False, closed=True):
127
+ user: bool
128
+
129
+ class BirthDayOrderByDict(TypedDict, total=False, closed=True):
130
+ user_id: Literal['asc', 'desc']
131
+ date: Literal['asc', 'desc']
132
+
133
+ class BirthDayTable:
134
+ model = BirthDay
135
+ insert_model = BirthDayInsert
136
+ datasource = DataSourceConfig(provider='sqlite', url='sqlite:///test.db', name=None)
137
+ column_specs: tuple[ColumnSpec, ...] = (
138
+ ColumnSpec(name='user_id', optional=False, auto_increment=False, has_default=False, has_default_factory=False),
139
+ ColumnSpec(name='date', optional=False, auto_increment=False, has_default=False, has_default_factory=False),
140
+ )
141
+ column_specs_by_name: Mapping[str, ColumnSpec] = MappingProxyType({spec.name: spec for spec in column_specs})
142
+ primary_key: tuple[str, ...] = ('user_id',)
143
+ indexes: tuple[tuple[str, ...], ...] = ()
144
+ unique_indexes: tuple[tuple[str, ...], ...] = ()
145
+ foreign_keys: tuple[ForeignKeySpec, ...] = (
146
+ ForeignKeySpec(
147
+ local_columns=('user_id',),
148
+ remote_model=User,
149
+ remote_columns=('id',),
150
+ backref='birthday',
151
+ ),
152
+ )
153
+
154
+ relations: tuple[RelationSpec, ...] = (
155
+ RelationSpec(name='user', table_name='UserTable', table_module=__name__, many=False, mapping=(('user_id', 'id'),), table_factory=lambda: UserTable),
156
+ )
157
+
158
+
159
+ def __init__(self, backend: BackendProtocol) -> None:
160
+ self._backend = backend
161
+
162
+ def insert(self, data: BirthDayInsert | BirthDayInsertDict) -> BirthDay:
163
+ return self._backend.insert(self, data)
164
+
165
+ def insert_many(self, data: Sequence[BirthDayInsert | BirthDayInsertDict], *, batch_size: int | None = None) -> list[BirthDay]:
166
+ return self._backend.insert_many(self, data, batch_size=batch_size)
167
+
168
+ def find_many(self, *, where: BirthDayWhereDict | None = None, include: BirthDayIncludeDict | None = None, order_by: BirthDayOrderByDict | None = None, take: int | None = None, skip: int | None = None) -> list[BirthDay]:
169
+ return self._backend.find_many(
170
+ self,
171
+ where=where, include=include, order_by=order_by,
172
+ take=take, skip=skip
173
+ )
174
+
175
+ def find_first(self, *, where: BirthDayWhereDict | None = None, include: BirthDayIncludeDict | None = None, order_by: BirthDayOrderByDict | None = None, skip: int | None = None) -> BirthDay | None:
176
+ return self._backend.find_first(
177
+ self,
178
+ where=where, include=include, order_by=order_by,
179
+ skip=skip
180
+ )
181
+
182
+ TBookIncludeCol = Literal['users']
183
+ TBookSortableCol = Literal['id', 'name']
184
+
185
+ @dataclass(slots=True, kw_only=True)
186
+ class BookInsert:
187
+ id: int | None = None
188
+ name: str
189
+
190
+
191
+ class BookInsertDict(TypedDict, closed=True):
192
+ id: NotRequired[int]
193
+ name: str
194
+
195
+
196
+ class BookWhereDict(TypedDict, total=False, closed=True):
197
+ id: int | None
198
+ name: str | None
199
+
200
+
201
+ class BookIncludeDict(TypedDict, total=False, closed=True):
202
+ users: bool
203
+
204
+ class BookOrderByDict(TypedDict, total=False, closed=True):
205
+ id: Literal['asc', 'desc']
206
+ name: Literal['asc', 'desc']
207
+
208
+ class BookTable:
209
+ model = Book
210
+ insert_model = BookInsert
211
+ datasource = DataSourceConfig(provider='sqlite', url='sqlite:///test.db', name=None)
212
+ column_specs: tuple[ColumnSpec, ...] = (
213
+ ColumnSpec(name='id', optional=False, auto_increment=True, has_default=False, has_default_factory=False),
214
+ ColumnSpec(name='name', optional=False, auto_increment=False, has_default=False, has_default_factory=False),
215
+ )
216
+ column_specs_by_name: Mapping[str, ColumnSpec] = MappingProxyType({spec.name: spec for spec in column_specs})
217
+ primary_key: tuple[str, ...] = ('id',)
218
+ indexes: tuple[tuple[str, ...], ...] = (('name',),)
219
+ unique_indexes: tuple[tuple[str, ...], ...] = ()
220
+ foreign_keys: tuple[ForeignKeySpec, ...] = ()
221
+
222
+ relations: tuple[RelationSpec, ...] = (
223
+ RelationSpec(name='users', table_name='UserBookTable', table_module=__name__, many=True, mapping=(('id', 'book_id'),), table_factory=lambda: UserBookTable),
224
+ )
225
+
226
+
227
+ def __init__(self, backend: BackendProtocol) -> None:
228
+ self._backend = backend
229
+
230
+ def insert(self, data: BookInsert | BookInsertDict) -> Book:
231
+ return self._backend.insert(self, data)
232
+
233
+ def insert_many(self, data: Sequence[BookInsert | BookInsertDict], *, batch_size: int | None = None) -> list[Book]:
234
+ return self._backend.insert_many(self, data, batch_size=batch_size)
235
+
236
+ def find_many(self, *, where: BookWhereDict | None = None, include: BookIncludeDict | None = None, order_by: BookOrderByDict | None = None, take: int | None = None, skip: int | None = None) -> list[Book]:
237
+ return self._backend.find_many(
238
+ self,
239
+ where=where, include=include, order_by=order_by,
240
+ take=take, skip=skip
241
+ )
242
+
243
+ def find_first(self, *, where: BookWhereDict | None = None, include: BookIncludeDict | None = None, order_by: BookOrderByDict | None = None, skip: int | None = None) -> Book | None:
244
+ return self._backend.find_first(
245
+ self,
246
+ where=where, include=include, order_by=order_by,
247
+ skip=skip
248
+ )
249
+
250
+ TUserIncludeCol = Literal['addresses', 'birthday', 'books']
251
+ TUserSortableCol = Literal['id', 'name', 'email', 'last_login']
252
+
253
+ @dataclass(slots=True, kw_only=True)
254
+ class UserInsert:
255
+ id: int | None = None
256
+ name: str
257
+ email: str
258
+ last_login: datetime
259
+
260
+
261
+ class UserInsertDict(TypedDict, closed=True):
262
+ id: NotRequired[int]
263
+ name: str
264
+ email: str
265
+ last_login: datetime
266
+
267
+
268
+ class UserWhereDict(TypedDict, total=False, closed=True):
269
+ id: int | None
270
+ name: str | None
271
+ email: str | None
272
+ last_login: datetime | None
273
+
274
+
275
+ class UserIncludeDict(TypedDict, total=False, closed=True):
276
+ addresses: bool
277
+ birthday: bool
278
+ books: bool
279
+
280
+ class UserOrderByDict(TypedDict, total=False, closed=True):
281
+ id: Literal['asc', 'desc']
282
+ name: Literal['asc', 'desc']
283
+ email: Literal['asc', 'desc']
284
+ last_login: Literal['asc', 'desc']
285
+
286
+ class UserTable:
287
+ model = User
288
+ insert_model = UserInsert
289
+ datasource = DataSourceConfig(provider='sqlite', url='sqlite:///test.db', name=None)
290
+ column_specs: tuple[ColumnSpec, ...] = (
291
+ ColumnSpec(name='id', optional=False, auto_increment=True, has_default=False, has_default_factory=False),
292
+ ColumnSpec(name='name', optional=False, auto_increment=False, has_default=False, has_default_factory=False),
293
+ ColumnSpec(name='email', optional=False, auto_increment=False, has_default=False, has_default_factory=False),
294
+ ColumnSpec(name='last_login', optional=False, auto_increment=False, has_default=False, has_default_factory=False),
295
+ )
296
+ column_specs_by_name: Mapping[str, ColumnSpec] = MappingProxyType({spec.name: spec for spec in column_specs})
297
+ primary_key: tuple[str, ...] = ('id',)
298
+ indexes: tuple[tuple[str, ...], ...] = (('name',), ('name', 'email'), ('last_login',),)
299
+ unique_indexes: tuple[tuple[str, ...], ...] = (('name', 'email'),)
300
+ foreign_keys: tuple[ForeignKeySpec, ...] = ()
301
+
302
+ relations: tuple[RelationSpec, ...] = (
303
+ RelationSpec(name='birthday', table_name='BirthDayTable', table_module=__name__, many=False, mapping=(('id', 'user_id'),), table_factory=lambda: BirthDayTable),
304
+ RelationSpec(name='addresses', table_name='AddressTable', table_module=__name__, many=True, mapping=(('id', 'user_id'),), table_factory=lambda: AddressTable),
305
+ RelationSpec(name='books', table_name='UserBookTable', table_module=__name__, many=True, mapping=(('id', 'user_id'),), table_factory=lambda: UserBookTable),
306
+ )
307
+
308
+
309
+ def __init__(self, backend: BackendProtocol) -> None:
310
+ self._backend = backend
311
+
312
+ def insert(self, data: UserInsert | UserInsertDict) -> User:
313
+ return self._backend.insert(self, data)
314
+
315
+ def insert_many(self, data: Sequence[UserInsert | UserInsertDict], *, batch_size: int | None = None) -> list[User]:
316
+ return self._backend.insert_many(self, data, batch_size=batch_size)
317
+
318
+ def find_many(self, *, where: UserWhereDict | None = None, include: UserIncludeDict | None = None, order_by: UserOrderByDict | None = None, take: int | None = None, skip: int | None = None) -> list[User]:
319
+ return self._backend.find_many(
320
+ self,
321
+ where=where, include=include, order_by=order_by,
322
+ take=take, skip=skip
323
+ )
324
+
325
+ def find_first(self, *, where: UserWhereDict | None = None, include: UserIncludeDict | None = None, order_by: UserOrderByDict | None = None, skip: int | None = None) -> User | None:
326
+ return self._backend.find_first(
327
+ self,
328
+ where=where, include=include, order_by=order_by,
329
+ skip=skip
330
+ )
331
+
332
+ TUserBookIncludeCol = Literal['book', 'user']
333
+ TUserBookSortableCol = Literal['user_id', 'book_id', 'created_at']
334
+
335
+ @dataclass(slots=True, kw_only=True)
336
+ class UserBookInsert:
337
+ user_id: int
338
+ book_id: int
339
+ created_at: datetime
340
+
341
+
342
+ class UserBookInsertDict(TypedDict, closed=True):
343
+ user_id: int
344
+ book_id: int
345
+ created_at: datetime
346
+
347
+
348
+ class UserBookWhereDict(TypedDict, total=False, closed=True):
349
+ user_id: int | None
350
+ book_id: int | None
351
+ created_at: datetime | None
352
+
353
+
354
+ class UserBookIncludeDict(TypedDict, total=False, closed=True):
355
+ book: bool
356
+ user: bool
357
+
358
+ class UserBookOrderByDict(TypedDict, total=False, closed=True):
359
+ user_id: Literal['asc', 'desc']
360
+ book_id: Literal['asc', 'desc']
361
+ created_at: Literal['asc', 'desc']
362
+
363
+ class UserBookTable:
364
+ model = UserBook
365
+ insert_model = UserBookInsert
366
+ datasource = DataSourceConfig(provider='sqlite', url='sqlite:///test.db', name=None)
367
+ column_specs: tuple[ColumnSpec, ...] = (
368
+ ColumnSpec(name='user_id', optional=False, auto_increment=False, has_default=False, has_default_factory=False),
369
+ ColumnSpec(name='book_id', optional=False, auto_increment=False, has_default=False, has_default_factory=False),
370
+ ColumnSpec(name='created_at', optional=False, auto_increment=False, has_default=False, has_default_factory=False),
371
+ )
372
+ column_specs_by_name: Mapping[str, ColumnSpec] = MappingProxyType({spec.name: spec for spec in column_specs})
373
+ primary_key: tuple[str, ...] = ('user_id', 'book_id')
374
+ indexes: tuple[tuple[str, ...], ...] = (('created_at',),)
375
+ unique_indexes: tuple[tuple[str, ...], ...] = ()
376
+ foreign_keys: tuple[ForeignKeySpec, ...] = (
377
+ ForeignKeySpec(
378
+ local_columns=('user_id',),
379
+ remote_model=User,
380
+ remote_columns=('id',),
381
+ backref='books',
382
+ ),
383
+ ForeignKeySpec(
384
+ local_columns=('book_id',),
385
+ remote_model=Book,
386
+ remote_columns=('id',),
387
+ backref='users',
388
+ ),
389
+ )
390
+
391
+ relations: tuple[RelationSpec, ...] = (
392
+ RelationSpec(name='user', table_name='UserTable', table_module=__name__, many=False, mapping=(('user_id', 'id'),), table_factory=lambda: UserTable),
393
+ RelationSpec(name='book', table_name='BookTable', table_module=__name__, many=False, mapping=(('book_id', 'id'),), table_factory=lambda: BookTable),
394
+ )
395
+
396
+
397
+ def __init__(self, backend: BackendProtocol) -> None:
398
+ self._backend = backend
399
+
400
+ def insert(self, data: UserBookInsert | UserBookInsertDict) -> UserBook:
401
+ return self._backend.insert(self, data)
402
+
403
+ def insert_many(self, data: Sequence[UserBookInsert | UserBookInsertDict], *, batch_size: int | None = None) -> list[UserBook]:
404
+ return self._backend.insert_many(self, data, batch_size=batch_size)
405
+
406
+ def find_many(self, *, where: UserBookWhereDict | None = None, include: UserBookIncludeDict | None = None, order_by: UserBookOrderByDict | None = None, take: int | None = None, skip: int | None = None) -> list[UserBook]:
407
+ return self._backend.find_many(
408
+ self,
409
+ where=where, include=include, order_by=order_by,
410
+ take=take, skip=skip
411
+ )
412
+
413
+ def find_first(self, *, where: UserBookWhereDict | None = None, include: UserBookIncludeDict | None = None, order_by: UserBookOrderByDict | None = None, skip: int | None = None) -> UserBook | None:
414
+ return self._backend.find_first(
415
+ self,
416
+ where=where, include=include, order_by=order_by,
417
+ skip=skip
418
+ )
419
+
420
+ class Client(BaseDBPool):
421
+ datasources = {
422
+ 'sqlite': DataSourceConfig(provider='sqlite', url='sqlite:///test.db', name=None),
423
+ }
424
+
425
+ @classmethod
426
+ @save_local
427
+ def _backend_sqlite(cls) -> BackendProtocol:
428
+ config = cls.datasources['sqlite']
429
+ if config.provider == 'sqlite':
430
+ from dclassql.runtime.backends.sqlite import SQLiteBackend
431
+ conn = open_sqlite_connection(config.url)
432
+ cls._setup_sqlite_db(conn)
433
+ return SQLiteBackend(conn)
434
+ raise ValueError(f"Unsupported provider '{config.provider}' for datasource 'sqlite'")
435
+
436
+ def __init__(self) -> None:
437
+ self.address = AddressTable(self._backend_sqlite())
438
+ self.birth_day = BirthDayTable(self._backend_sqlite())
439
+ self.book = BookTable(self._backend_sqlite())
440
+ self.user = UserTable(self._backend_sqlite())
441
+ self.user_book = UserBookTable(self._backend_sqlite())
442
+
443
+ @classmethod
444
+ def close_all(cls, verbose: bool = False) -> None:
445
+ super().close_all(verbose=verbose)
446
+ if hasattr(cls._local, '_backend_sqlite'):
447
+ backend = getattr(cls._local, '_backend_sqlite')
448
+ if hasattr(backend, 'close') and callable(getattr(backend, 'close')):
449
+ backend.close()
450
+ delattr(cls._local, '_backend_sqlite')
451
+
452
+ __all__ = (
453
+ "DataSourceConfig",
454
+ "ForeignKeySpec",
455
+ "Client",
456
+ "TAddressIncludeCol",
457
+ "TAddressSortableCol",
458
+ "AddressIncludeDict",
459
+ "AddressOrderByDict",
460
+ "AddressInsert",
461
+ "AddressInsertDict",
462
+ "AddressWhereDict",
463
+ "AddressTable",
464
+ "TBirthDayIncludeCol",
465
+ "TBirthDaySortableCol",
466
+ "BirthDayIncludeDict",
467
+ "BirthDayOrderByDict",
468
+ "BirthDayInsert",
469
+ "BirthDayInsertDict",
470
+ "BirthDayWhereDict",
471
+ "BirthDayTable",
472
+ "TBookIncludeCol",
473
+ "TBookSortableCol",
474
+ "BookIncludeDict",
475
+ "BookOrderByDict",
476
+ "BookInsert",
477
+ "BookInsertDict",
478
+ "BookWhereDict",
479
+ "BookTable",
480
+ "TUserIncludeCol",
481
+ "TUserSortableCol",
482
+ "UserIncludeDict",
483
+ "UserOrderByDict",
484
+ "UserInsert",
485
+ "UserInsertDict",
486
+ "UserWhereDict",
487
+ "UserTable",
488
+ "TUserBookIncludeCol",
489
+ "TUserBookSortableCol",
490
+ "UserBookIncludeDict",
491
+ "UserBookOrderByDict",
492
+ "UserBookInsert",
493
+ "UserBookInsertDict",
494
+ "UserBookWhereDict",
495
+ "UserBookTable",
496
+ )