dclassql 0.1.5__tar.gz → 0.2.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.5 → dclassql-0.2.0}/PKG-INFO +1 -1
- {dclassql-0.1.5 → dclassql-0.2.0}/pyproject.toml +1 -1
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/__init__.py +6 -1
- dclassql-0.2.0/src/dclassql/asdict.py +192 -0
- dclassql-0.2.0/src/dclassql/asdict.pyi +57 -0
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/cli.py +26 -3
- dclassql-0.2.0/src/dclassql/client.py +863 -0
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/codegen.py +87 -4
- dclassql-0.2.0/src/dclassql/generated_models/__init__.py +0 -0
- dclassql-0.2.0/src/dclassql/generated_models/test_models.py +78 -0
- dclassql-0.2.0/src/dclassql/runtime/backends/base.py +620 -0
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/runtime/backends/lazy.py +17 -2
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/runtime/backends/protocols.py +68 -2
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/runtime/backends/sqlite.py +66 -0
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/runtime/backends/where_compiler.py +5 -3
- dclassql-0.2.0/src/dclassql/runtime/sql_recorder.py +44 -0
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/table_spec.py +4 -1
- dclassql-0.2.0/src/dclassql/templates/asdict_stub.pyi.jinja +27 -0
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/templates/partials/imports.jinja +1 -1
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/templates/partials/model_section.jinja +78 -7
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/typing.py +1 -0
- dclassql-0.1.5/src/dclassql/runtime/backends/base.py +0 -300
- {dclassql-0.1.5 → dclassql-0.2.0}/README.md +0 -0
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/.gitignore +0 -0
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/db_pool.py +0 -0
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/model_inspector.py +0 -0
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/push/__init__.py +0 -0
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/push/base.py +0 -0
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/push/sqlite.py +0 -0
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/runtime/backends/__init__.py +0 -0
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/runtime/backends/metadata.py +0 -0
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/runtime/datasource.py +0 -0
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/runtime/sqlite_adapters.py +0 -0
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/templates/__init__.py +0 -0
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/templates/client_module.py.jinja +0 -0
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/templates/partials/client_class.jinja +0 -0
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/templates/partials/exports.jinja +0 -0
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/templates/partials/macros.jinja +0 -0
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/templates/partials/scalar_filters.jinja +0 -0
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/unwarp.py +0 -0
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/utils/__init__.py +0 -0
- {dclassql-0.1.5 → dclassql-0.2.0}/src/dclassql/utils/ensure.py +0 -0
|
@@ -1,9 +1,12 @@
|
|
|
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
|
|
6
|
+
from .runtime.sql_recorder import record_sql
|
|
5
7
|
from .unwarp import unwarp, unwarp_or, unwarp_or_raise
|
|
6
8
|
|
|
9
|
+
|
|
7
10
|
class _MissingClient:
|
|
8
11
|
def __init__(self, *args: object, **kwargs: object) -> None:
|
|
9
12
|
raise RuntimeError(
|
|
@@ -20,10 +23,12 @@ __all__ = [
|
|
|
20
23
|
'Client',
|
|
21
24
|
'db_push',
|
|
22
25
|
'eager',
|
|
26
|
+
'asdict',
|
|
23
27
|
'unwarp',
|
|
24
28
|
'unwarp_or',
|
|
25
29
|
'unwarp_or_raise',
|
|
26
30
|
'BaseDBPool',
|
|
27
31
|
'save_local',
|
|
28
32
|
'DataSourceConfig',
|
|
33
|
+
'record_sql',
|
|
29
34
|
]
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses as _dataclasses
|
|
4
|
+
from collections.abc import Mapping, Sequence
|
|
5
|
+
from dataclasses import fields, is_dataclass
|
|
6
|
+
from typing import Any, Literal, cast
|
|
7
|
+
|
|
8
|
+
from .runtime.backends.lazy import (
|
|
9
|
+
LAZY_RELATION_STATE,
|
|
10
|
+
LazyInstance,
|
|
11
|
+
LazyRelationState,
|
|
12
|
+
resolve_lazy_relation,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
RelationPolicy = Literal['skip', 'fetch', 'keep']
|
|
16
|
+
RelationKey = tuple[type[Any] | None, tuple[tuple[str, Any], ...]]
|
|
17
|
+
_SEQUENCE_SKIP_TYPES = (str, bytes, bytearray)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def asdict(value: Any, *, relation_policy: RelationPolicy = 'keep') -> Any:
|
|
21
|
+
if value is None:
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
memo: set[int] = set()
|
|
25
|
+
relation_guard: set[RelationKey] = set()
|
|
26
|
+
return _convert_value(value, relation_policy, memo, relation_guard)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _convert_value(
|
|
30
|
+
value: Any,
|
|
31
|
+
relation_policy: RelationPolicy,
|
|
32
|
+
memo: set[int],
|
|
33
|
+
relation_guard: set[RelationKey],
|
|
34
|
+
) -> Any:
|
|
35
|
+
if value is None:
|
|
36
|
+
return None
|
|
37
|
+
if is_dataclass(value):
|
|
38
|
+
return _convert_dataclass(value, relation_policy, memo, relation_guard)
|
|
39
|
+
if isinstance(value, LazyInstance):
|
|
40
|
+
resolved = value._lazy_resolve()
|
|
41
|
+
return _convert_value(resolved, relation_policy, memo, relation_guard)
|
|
42
|
+
if isinstance(value, Mapping):
|
|
43
|
+
return {
|
|
44
|
+
key: _convert_value(item, relation_policy, memo, relation_guard)
|
|
45
|
+
for key, item in value.items()
|
|
46
|
+
}
|
|
47
|
+
if isinstance(value, list):
|
|
48
|
+
return [_convert_value(item, relation_policy, memo, relation_guard) for item in value]
|
|
49
|
+
if isinstance(value, tuple):
|
|
50
|
+
return tuple(_convert_value(item, relation_policy, memo, relation_guard) for item in value)
|
|
51
|
+
if isinstance(value, Sequence) and not isinstance(value, _SEQUENCE_SKIP_TYPES):
|
|
52
|
+
return [_convert_value(item, relation_policy, memo, relation_guard) for item in value]
|
|
53
|
+
return value
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _convert_dataclass(
|
|
57
|
+
instance: Any,
|
|
58
|
+
relation_policy: RelationPolicy,
|
|
59
|
+
memo: set[int],
|
|
60
|
+
relation_guard: set[RelationKey],
|
|
61
|
+
) -> dict[str, Any]:
|
|
62
|
+
instance_id = id(instance)
|
|
63
|
+
if instance_id in memo:
|
|
64
|
+
raise RecursionError('dclassql.asdict() detected a recursive dataclass reference')
|
|
65
|
+
memo.add(instance_id)
|
|
66
|
+
try:
|
|
67
|
+
state_map = LAZY_RELATION_STATE.get(instance)
|
|
68
|
+
result: dict[str, Any] = {}
|
|
69
|
+
for field_obj in fields(instance):
|
|
70
|
+
name = field_obj.name
|
|
71
|
+
state = _lookup_relation_state(state_map, name)
|
|
72
|
+
if state is not None:
|
|
73
|
+
result[name] = _convert_relation(instance, state, relation_policy, memo, relation_guard)
|
|
74
|
+
continue
|
|
75
|
+
value = getattr(instance, name)
|
|
76
|
+
result[name] = _convert_value(value, relation_policy, memo, relation_guard)
|
|
77
|
+
return result
|
|
78
|
+
finally:
|
|
79
|
+
memo.remove(instance_id)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _lookup_relation_state(state_map: dict[str, LazyRelationState] | None, name: str) -> LazyRelationState | None:
|
|
83
|
+
if state_map is None:
|
|
84
|
+
return None
|
|
85
|
+
return state_map.get(name)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _convert_relation(
|
|
89
|
+
owner: Any,
|
|
90
|
+
state: LazyRelationState,
|
|
91
|
+
relation_policy: RelationPolicy,
|
|
92
|
+
memo: set[int],
|
|
93
|
+
relation_guard: set[RelationKey],
|
|
94
|
+
) -> Any:
|
|
95
|
+
relation_key = _relation_identity(owner, state)
|
|
96
|
+
if relation_key is not None and relation_key in relation_guard:
|
|
97
|
+
return [] if state.many else None
|
|
98
|
+
|
|
99
|
+
guard_added = False
|
|
100
|
+
if relation_key is not None:
|
|
101
|
+
relation_guard.add(relation_key)
|
|
102
|
+
guard_added = True
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
if relation_policy == 'skip':
|
|
106
|
+
return [] if state.many else None
|
|
107
|
+
|
|
108
|
+
if relation_policy == 'fetch':
|
|
109
|
+
value = resolve_lazy_relation(owner, state)
|
|
110
|
+
elif state.loaded:
|
|
111
|
+
value = state.value
|
|
112
|
+
else:
|
|
113
|
+
if relation_policy == 'keep':
|
|
114
|
+
return [] if state.many else None
|
|
115
|
+
value = resolve_lazy_relation(owner, state)
|
|
116
|
+
|
|
117
|
+
if value is None:
|
|
118
|
+
return None if not state.many else []
|
|
119
|
+
|
|
120
|
+
if state.many:
|
|
121
|
+
result_list: list[Any] = []
|
|
122
|
+
for item in value:
|
|
123
|
+
if is_dataclass(item) and id(item) in memo:
|
|
124
|
+
continue
|
|
125
|
+
result_list.append(_convert_value(item, relation_policy, memo, relation_guard))
|
|
126
|
+
return result_list
|
|
127
|
+
|
|
128
|
+
if is_dataclass(value) and id(value) in memo:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
return _convert_value(value, relation_policy, memo, relation_guard)
|
|
132
|
+
finally:
|
|
133
|
+
if guard_added and relation_key is not None:
|
|
134
|
+
relation_guard.discard(relation_key)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _relation_identity(owner: Any, state: LazyRelationState) -> RelationKey | None:
|
|
138
|
+
mapping = state.mapping
|
|
139
|
+
if not mapping:
|
|
140
|
+
return None
|
|
141
|
+
values: list[tuple[str, Any]] = []
|
|
142
|
+
for owner_column, target_column in mapping:
|
|
143
|
+
owner_value = getattr(owner, owner_column, None)
|
|
144
|
+
if owner_value is None:
|
|
145
|
+
return None
|
|
146
|
+
values.append((target_column, owner_value))
|
|
147
|
+
model_cls = getattr(state.table_cls, 'model', None)
|
|
148
|
+
return (model_cls, tuple(values))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
__all__ = ['RelationPolicy', 'asdict']
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _apply_dict_factory(value: Any, dict_factory: Any) -> Any:
|
|
155
|
+
if isinstance(value, dict):
|
|
156
|
+
return dict_factory((key, _apply_dict_factory(val, dict_factory)) for key, val in value.items())
|
|
157
|
+
if isinstance(value, list):
|
|
158
|
+
return [_apply_dict_factory(item, dict_factory) for item in value]
|
|
159
|
+
if isinstance(value, tuple):
|
|
160
|
+
return tuple(_apply_dict_factory(item, dict_factory) for item in value)
|
|
161
|
+
if isinstance(value, set):
|
|
162
|
+
return {_apply_dict_factory(item, dict_factory) for item in value}
|
|
163
|
+
return value
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _patch_dataclasses_asdict() -> None:
|
|
167
|
+
original = getattr(_dataclasses, '_dclassql_original_asdict_inner', None)
|
|
168
|
+
if original is not None:
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
original_inner = cast(Any, getattr(_dataclasses, '_asdict_inner'))
|
|
172
|
+
|
|
173
|
+
def _patched_inner(obj: Any, dict_factory: Any):
|
|
174
|
+
obj_in_state = False
|
|
175
|
+
try:
|
|
176
|
+
# some objects may raise exceptions on __hash__/__eq__
|
|
177
|
+
obj_in_state = obj in LAZY_RELATION_STATE
|
|
178
|
+
except Exception:
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
if obj_in_state:
|
|
182
|
+
data = asdict(obj)
|
|
183
|
+
if dict_factory is dict:
|
|
184
|
+
return data
|
|
185
|
+
return _apply_dict_factory(data, dict_factory)
|
|
186
|
+
return original_inner(obj, dict_factory)
|
|
187
|
+
|
|
188
|
+
setattr(_dataclasses, '_asdict_inner', _patched_inner)
|
|
189
|
+
setattr(_dataclasses, '_dclassql_original_asdict_inner', original_inner)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
_patch_dataclasses_asdict()
|
|
@@ -0,0 +1,57 @@
|
|
|
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: ...
|
|
@@ -28,11 +28,27 @@ ConfirmCallback = Callable[
|
|
|
28
28
|
]
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
def _module_name_from_path(module_path: Path) -> str:
|
|
32
|
+
candidate = module_path.with_suffix("")
|
|
33
|
+
cwd = Path.cwd()
|
|
34
|
+
try:
|
|
35
|
+
rel = candidate.relative_to(cwd)
|
|
36
|
+
parts = rel.parts
|
|
37
|
+
except ValueError:
|
|
38
|
+
parts = candidate.parts
|
|
39
|
+
trimmed_parts = list(parts)
|
|
40
|
+
|
|
41
|
+
if not trimmed_parts:
|
|
42
|
+
trimmed_parts = [module_path.stem]
|
|
43
|
+
|
|
44
|
+
sanitized_parts = [re.sub(r"[^0-9a-zA-Z_]+", "_", part) or "_" for part in trimmed_parts]
|
|
45
|
+
return ".".join(sanitized_parts)
|
|
46
|
+
|
|
31
47
|
def load_module(module_path: Path) -> ModuleType:
|
|
32
48
|
module_path = module_path.resolve()
|
|
33
49
|
if not module_path.exists():
|
|
34
50
|
raise FileNotFoundError(f"Model file '{module_path}' does not exist")
|
|
35
|
-
module_name = module_path
|
|
51
|
+
module_name = _module_name_from_path(module_path)
|
|
36
52
|
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
|
37
53
|
if spec is None or spec.loader is None:
|
|
38
54
|
raise ImportError(f"Unable to load module from '{module_path}'")
|
|
@@ -57,6 +73,10 @@ def resolve_generated_path() -> Path:
|
|
|
57
73
|
return _find_package_directory() / GENERATED_CLIENT_FILENAME
|
|
58
74
|
|
|
59
75
|
|
|
76
|
+
def resolve_asdict_stub_path() -> Path:
|
|
77
|
+
return _find_package_directory() / "asdict.pyi"
|
|
78
|
+
|
|
79
|
+
|
|
60
80
|
def resolve_models_directory() -> Path:
|
|
61
81
|
return _find_package_directory() / GENERATED_MODELS_DIRNAME
|
|
62
82
|
|
|
@@ -195,6 +215,8 @@ def command_generate(module_path: Path) -> None:
|
|
|
195
215
|
output_path = resolve_generated_path()
|
|
196
216
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
197
217
|
output_path.write_text(generated.code, encoding="utf-8")
|
|
218
|
+
asdict_stub_path = resolve_asdict_stub_path()
|
|
219
|
+
asdict_stub_path.write_text(generated.asdict_stub, encoding="utf-8")
|
|
198
220
|
sys.stdout.write(f"Client written to {output_path}\n")
|
|
199
221
|
|
|
200
222
|
|
|
@@ -256,8 +278,9 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
256
278
|
try:
|
|
257
279
|
handler(args)
|
|
258
280
|
return 0
|
|
259
|
-
except Exception
|
|
260
|
-
|
|
281
|
+
except Exception: # pragma: no cover - CLI error reporting
|
|
282
|
+
import traceback
|
|
283
|
+
traceback.print_exc(file=sys.stderr)
|
|
261
284
|
return 1
|
|
262
285
|
|
|
263
286
|
|