rakko-orm 1.0.0a0__py3-none-any.whl
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.
- rakko/__init__.py +613 -0
- rakko/__main__.py +10 -0
- rakko/apps.py +356 -0
- rakko/backends/__init__.py +4 -0
- rakko/backends/asyncpg/__init__.py +7 -0
- rakko/backends/asyncpg/client.py +223 -0
- rakko/backends/asyncpg/executor.py +20 -0
- rakko/backends/asyncpg/schema_generator.py +17 -0
- rakko/backends/base/__init__.py +4 -0
- rakko/backends/base/client.py +438 -0
- rakko/backends/base/config_generator.py +249 -0
- rakko/backends/base/executor.py +764 -0
- rakko/backends/base/schema_generator.py +580 -0
- rakko/backends/base_postgres/__init__.py +4 -0
- rakko/backends/base_postgres/client.py +170 -0
- rakko/backends/base_postgres/executor.py +149 -0
- rakko/backends/base_postgres/schema_generator.py +84 -0
- rakko/backends/mssql/__init__.py +7 -0
- rakko/backends/mssql/client.py +127 -0
- rakko/backends/mssql/executor.py +17 -0
- rakko/backends/mssql/schema_generator.py +125 -0
- rakko/backends/mysql/__init__.py +7 -0
- rakko/backends/mysql/client.py +312 -0
- rakko/backends/mysql/executor.py +154 -0
- rakko/backends/mysql/schema_generator.py +101 -0
- rakko/backends/odbc/__init__.py +4 -0
- rakko/backends/odbc/client.py +221 -0
- rakko/backends/odbc/executor.py +20 -0
- rakko/backends/oracle/__init__.py +7 -0
- rakko/backends/oracle/client.py +136 -0
- rakko/backends/oracle/executor.py +27 -0
- rakko/backends/oracle/schema_generator.py +120 -0
- rakko/backends/psycopg/__init__.py +7 -0
- rakko/backends/psycopg/client.py +288 -0
- rakko/backends/psycopg/executor.py +14 -0
- rakko/backends/psycopg/schema_generator.py +17 -0
- rakko/backends/sqlite/__init__.py +14 -0
- rakko/backends/sqlite/client.py +299 -0
- rakko/backends/sqlite/executor.py +88 -0
- rakko/backends/sqlite/schema_generator.py +32 -0
- rakko/cli/__init__.py +13 -0
- rakko/cli/cli.py +878 -0
- rakko/cli/utils.py +181 -0
- rakko/config.py +315 -0
- rakko/connection.py +345 -0
- rakko/context.py +571 -0
- rakko/contrib/__init__.py +4 -0
- rakko/contrib/aiohttp/__init__.py +99 -0
- rakko/contrib/blacksheep/__init__.py +121 -0
- rakko/contrib/fastapi/__init__.py +301 -0
- rakko/contrib/mysql/__init__.py +4 -0
- rakko/contrib/mysql/fields.py +63 -0
- rakko/contrib/mysql/functions.py +19 -0
- rakko/contrib/mysql/indexes.py +28 -0
- rakko/contrib/mysql/json_functions.py +80 -0
- rakko/contrib/mysql/search.py +49 -0
- rakko/contrib/postgres/__init__.py +4 -0
- rakko/contrib/postgres/array_functions.py +35 -0
- rakko/contrib/postgres/fields.py +105 -0
- rakko/contrib/postgres/functions.py +43 -0
- rakko/contrib/postgres/indexes.py +33 -0
- rakko/contrib/postgres/json_functions.py +113 -0
- rakko/contrib/postgres/regex.py +31 -0
- rakko/contrib/postgres/search.py +483 -0
- rakko/contrib/pydantic/__init__.py +16 -0
- rakko/contrib/pydantic/base.py +147 -0
- rakko/contrib/pydantic/creator.py +681 -0
- rakko/contrib/pydantic/descriptions.py +207 -0
- rakko/contrib/pydantic/utils.py +38 -0
- rakko/contrib/pylint/__init__.py +158 -0
- rakko/contrib/quart/__init__.py +117 -0
- rakko/contrib/sanic/__init__.py +116 -0
- rakko/contrib/sqlite/__init__.py +4 -0
- rakko/contrib/sqlite/functions.py +16 -0
- rakko/contrib/sqlite/regex.py +46 -0
- rakko/contrib/starlette/__init__.py +180 -0
- rakko/contrib/test/__init__.py +336 -0
- rakko/contrib/test/condition.py +42 -0
- rakko/converters.py +160 -0
- rakko/exceptions.py +130 -0
- rakko/expressions.py +962 -0
- rakko/fields/__init__.py +111 -0
- rakko/fields/base.py +685 -0
- rakko/fields/data.py +1308 -0
- rakko/fields/db_defaults.py +105 -0
- rakko/fields/relational.py +703 -0
- rakko/filters.py +639 -0
- rakko/functions.py +142 -0
- rakko/indexes.py +165 -0
- rakko/log.py +8 -0
- rakko/manager.py +29 -0
- rakko/migrations/__init__.py +41 -0
- rakko/migrations/api/__init__.py +16 -0
- rakko/migrations/api/migrate.py +77 -0
- rakko/migrations/api/plan.py +76 -0
- rakko/migrations/api/sqlmigrate.py +69 -0
- rakko/migrations/autodetector.py +203 -0
- rakko/migrations/constraints.py +31 -0
- rakko/migrations/exceptions.py +10 -0
- rakko/migrations/executor.py +368 -0
- rakko/migrations/expressions.py +13 -0
- rakko/migrations/graph.py +158 -0
- rakko/migrations/loader.py +127 -0
- rakko/migrations/migration.py +189 -0
- rakko/migrations/operations.py +1288 -0
- rakko/migrations/recorder.py +104 -0
- rakko/migrations/schema_editor/__init__.py +25 -0
- rakko/migrations/schema_editor/asyncpg.py +11 -0
- rakko/migrations/schema_editor/base.py +945 -0
- rakko/migrations/schema_editor/base_postgres.py +171 -0
- rakko/migrations/schema_editor/data.py +18 -0
- rakko/migrations/schema_editor/mssql.py +339 -0
- rakko/migrations/schema_editor/mysql.py +408 -0
- rakko/migrations/schema_editor/oracle.py +259 -0
- rakko/migrations/schema_editor/psycopg.py +11 -0
- rakko/migrations/schema_editor/sqlite.py +424 -0
- rakko/migrations/schema_generator/__init__.py +5 -0
- rakko/migrations/schema_generator/operation_generator.py +163 -0
- rakko/migrations/schema_generator/state.py +224 -0
- rakko/migrations/schema_generator/state_apps.py +165 -0
- rakko/migrations/schema_generator/state_diff.py +419 -0
- rakko/migrations/writer.py +515 -0
- rakko/models.py +1854 -0
- rakko/py.typed +0 -0
- rakko/query_api.py +134 -0
- rakko/query_utils.py +238 -0
- rakko/queryset.py +2743 -0
- rakko/relations.py +85 -0
- rakko/router.py +55 -0
- rakko/schema_quoting.py +62 -0
- rakko/signals.py +73 -0
- rakko/timezone.py +173 -0
- rakko/transactions.py +69 -0
- rakko/utils.py +50 -0
- rakko/validators.py +412 -0
- rakko/warnings.py +22 -0
- rakko_orm-1.0.0a0.dist-info/METADATA +114 -0
- rakko_orm-1.0.0a0.dist-info/RECORD +143 -0
- rakko_orm-1.0.0a0.dist-info/WHEEL +4 -0
- rakko_orm-1.0.0a0.dist-info/entry_points.txt +5 -0
- rakko_orm-1.0.0a0.dist-info/licenses/CONTRIBUTORS.rst +86 -0
- rakko_orm-1.0.0a0.dist-info/licenses/LICENSE.txt +201 -0
- rakko_orm-1.0.0a0.dist-info/licenses/NOTICE.txt +22 -0
rakko/apps.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# Copyright (c) 2026 ropmyung (Rakko ORM modifications and additions).
|
|
2
|
+
# Derived from Tortoise ORM; maintained and modified for Rakko ORM.
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import importlib
|
|
8
|
+
import warnings
|
|
9
|
+
from collections.abc import Callable, Iterable, Iterator
|
|
10
|
+
from copy import copy
|
|
11
|
+
from inspect import isclass
|
|
12
|
+
from itertools import chain
|
|
13
|
+
from types import ModuleType
|
|
14
|
+
from typing import Any, cast
|
|
15
|
+
|
|
16
|
+
from pypika_tortoise import Table
|
|
17
|
+
from pypika_tortoise.queries import QueryBuilder
|
|
18
|
+
|
|
19
|
+
from rakko.connection import ConnectionHandler
|
|
20
|
+
from rakko.exceptions import ConfigurationError
|
|
21
|
+
from rakko.fields.relational import (
|
|
22
|
+
BackwardFKRelation,
|
|
23
|
+
BackwardOneToOneRelation,
|
|
24
|
+
ForeignKeyFieldInstance,
|
|
25
|
+
ManyToManyFieldInstance,
|
|
26
|
+
OneToOneFieldInstance,
|
|
27
|
+
)
|
|
28
|
+
from rakko.filters import get_m2m_filters
|
|
29
|
+
from rakko.models import Model
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Apps:
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
config: dict[str, dict[str, Any]] | None,
|
|
36
|
+
connections: ConnectionHandler,
|
|
37
|
+
table_name_generator: Callable[[type[Model]], str] | None = None,
|
|
38
|
+
*,
|
|
39
|
+
validate_connections: bool = True,
|
|
40
|
+
) -> None:
|
|
41
|
+
self.apps: dict[str, dict[str, type[Model]]] = {}
|
|
42
|
+
self._config = config or {}
|
|
43
|
+
self._connections = connections
|
|
44
|
+
self._table_name_generator = table_name_generator
|
|
45
|
+
self._validate_connections = validate_connections
|
|
46
|
+
if self._config:
|
|
47
|
+
self._load_from_config()
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def _discover_models(models_path: ModuleType | str, app_label: str) -> list[type[Model]]:
|
|
51
|
+
if isinstance(models_path, ModuleType):
|
|
52
|
+
module = models_path
|
|
53
|
+
else:
|
|
54
|
+
try:
|
|
55
|
+
module = importlib.import_module(models_path)
|
|
56
|
+
except ImportError:
|
|
57
|
+
raise ConfigurationError(f'Module "{models_path}" not found')
|
|
58
|
+
discovered_models: list[type[Model]] = []
|
|
59
|
+
if possible_models := getattr(module, "__models__", None):
|
|
60
|
+
try:
|
|
61
|
+
possible_models = [*possible_models]
|
|
62
|
+
except TypeError:
|
|
63
|
+
possible_models = None
|
|
64
|
+
if not possible_models:
|
|
65
|
+
possible_models = [getattr(module, attr_name) for attr_name in dir(module)]
|
|
66
|
+
for attr in possible_models:
|
|
67
|
+
if isclass(attr) and issubclass(attr, Model) and not attr._meta.abstract:
|
|
68
|
+
if attr._meta.app and attr._meta.app != app_label:
|
|
69
|
+
continue
|
|
70
|
+
attr._meta.app = app_label
|
|
71
|
+
discovered_models.append(attr)
|
|
72
|
+
if not discovered_models:
|
|
73
|
+
warnings.warn(f'Module "{models_path}" has no models', RuntimeWarning, stacklevel=4)
|
|
74
|
+
return discovered_models
|
|
75
|
+
|
|
76
|
+
def init_app(
|
|
77
|
+
self,
|
|
78
|
+
label: str,
|
|
79
|
+
module_list: Iterable[ModuleType | str],
|
|
80
|
+
_init_relations: bool = True,
|
|
81
|
+
) -> dict[str, type[Model]]:
|
|
82
|
+
app_models: list[type[Model]] = []
|
|
83
|
+
for module in module_list:
|
|
84
|
+
app_models += self._discover_models(module, label)
|
|
85
|
+
|
|
86
|
+
self.apps[label] = {model.__name__: model for model in app_models}
|
|
87
|
+
|
|
88
|
+
if _init_relations:
|
|
89
|
+
self._init_relations()
|
|
90
|
+
|
|
91
|
+
return self.apps[label]
|
|
92
|
+
|
|
93
|
+
def _load_from_config(self) -> None:
|
|
94
|
+
if self._connections is None:
|
|
95
|
+
raise ConfigurationError("ConnectionHandler is required to load from config")
|
|
96
|
+
for name, info in self._config.items():
|
|
97
|
+
default_connection = info.get("default_connection", "default")
|
|
98
|
+
if self._validate_connections:
|
|
99
|
+
try:
|
|
100
|
+
self._connections.get(default_connection)
|
|
101
|
+
except KeyError:
|
|
102
|
+
raise ConfigurationError(
|
|
103
|
+
f'Unknown connection "{default_connection}" for app "{name}"'
|
|
104
|
+
)
|
|
105
|
+
else:
|
|
106
|
+
if default_connection not in self._connections.db_config:
|
|
107
|
+
raise ConfigurationError(
|
|
108
|
+
f'Unknown connection "{default_connection}" for app "{name}"'
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
self.init_app(name, info["models"], _init_relations=False)
|
|
112
|
+
|
|
113
|
+
for model in self.apps[name].values():
|
|
114
|
+
model._meta.default_connection = default_connection
|
|
115
|
+
|
|
116
|
+
self._init_relations()
|
|
117
|
+
if self._validate_connections:
|
|
118
|
+
self._build_initial_querysets()
|
|
119
|
+
|
|
120
|
+
def _build_initial_querysets(self) -> None:
|
|
121
|
+
for app in self.apps.values():
|
|
122
|
+
for model in app.values():
|
|
123
|
+
model._meta.finalise_model()
|
|
124
|
+
model._meta.basetable = Table(name=model._meta.db_table, schema=model._meta.schema)
|
|
125
|
+
basequery = model._meta.db.query_class.from_(model._meta.basetable)
|
|
126
|
+
model._meta.basequery = cast(QueryBuilder, basequery)
|
|
127
|
+
model._meta.basequery_all_fields = cast(
|
|
128
|
+
QueryBuilder, basequery.select(*model._meta.db_fields)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def _init_relations(self) -> None:
|
|
132
|
+
def get_related_model(related_app_name: str, related_model_name: str) -> type[Model]:
|
|
133
|
+
"""
|
|
134
|
+
Test, if app and model really exist. Throws a ConfigurationError with a hopefully
|
|
135
|
+
helpful message. If successful, returns the requested model.
|
|
136
|
+
|
|
137
|
+
:raises ConfigurationError: If no such app exists.
|
|
138
|
+
"""
|
|
139
|
+
try:
|
|
140
|
+
return self.apps[related_app_name][related_model_name]
|
|
141
|
+
except KeyError:
|
|
142
|
+
if related_app_name not in self.apps:
|
|
143
|
+
raise ConfigurationError(
|
|
144
|
+
f"No app with name '{related_app_name}' registered."
|
|
145
|
+
f" Please check your model names in ForeignKeyFields"
|
|
146
|
+
f" and configurations."
|
|
147
|
+
)
|
|
148
|
+
raise ConfigurationError(
|
|
149
|
+
f"No model with name '{related_model_name}' registered in"
|
|
150
|
+
f" app '{related_app_name}'."
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def split_reference(reference: str) -> tuple[str, str]:
|
|
154
|
+
"""
|
|
155
|
+
Validate, if reference follow the official naming conventions. Throws a
|
|
156
|
+
ConfigurationError with a hopefully helpful message. If successful,
|
|
157
|
+
returns the app and the model name.
|
|
158
|
+
|
|
159
|
+
:raises ConfigurationError: If reference is invalid.
|
|
160
|
+
"""
|
|
161
|
+
if len(items := reference.split(".")) != 2: # pragma: nocoverage
|
|
162
|
+
raise ConfigurationError(
|
|
163
|
+
f"'{reference}' is not a valid model reference Bad Reference."
|
|
164
|
+
" Should be something like '<appname>.<modelname>'."
|
|
165
|
+
)
|
|
166
|
+
return items[0], items[1]
|
|
167
|
+
|
|
168
|
+
def init_fk_o2o_field(model: type[Model], field: str, is_o2o: bool = False) -> None:
|
|
169
|
+
fk_object = cast(
|
|
170
|
+
"OneToOneFieldInstance | ForeignKeyFieldInstance", model._meta.fields_map[field]
|
|
171
|
+
)
|
|
172
|
+
reference = fk_object.model_name
|
|
173
|
+
if not isinstance(reference, str):
|
|
174
|
+
related_model: type[Model] = reference
|
|
175
|
+
related_model_name = related_model.__name__
|
|
176
|
+
else:
|
|
177
|
+
related_app_name, related_model_name = split_reference(reference)
|
|
178
|
+
related_model = get_related_model(related_app_name, related_model_name)
|
|
179
|
+
|
|
180
|
+
if to_field := fk_object.to_field:
|
|
181
|
+
related_field = related_model._meta.fields_map.get(to_field)
|
|
182
|
+
if not related_field:
|
|
183
|
+
raise ConfigurationError(
|
|
184
|
+
f'there is no field named "{to_field}" in model "{related_model_name}"'
|
|
185
|
+
)
|
|
186
|
+
if not related_field.unique:
|
|
187
|
+
raise ConfigurationError(
|
|
188
|
+
f'field "{to_field}" in model "{related_model_name}" is not unique'
|
|
189
|
+
)
|
|
190
|
+
else:
|
|
191
|
+
fk_object.to_field = related_model._meta.pk_attr
|
|
192
|
+
related_field = related_model._meta.pk
|
|
193
|
+
key_fk_object = copy(related_field)
|
|
194
|
+
fk_object.to_field_instance = related_field
|
|
195
|
+
fk_object.field_type = fk_object.to_field_instance.field_type
|
|
196
|
+
|
|
197
|
+
key_field = f"{field}_id"
|
|
198
|
+
key_fk_object.reference = fk_object
|
|
199
|
+
key_fk_object.source_field = fk_object.source_field or key_field
|
|
200
|
+
for attr in ("index", "default", "null", "generated", "description", "db_default"):
|
|
201
|
+
setattr(key_fk_object, attr, getattr(fk_object, attr))
|
|
202
|
+
if is_o2o:
|
|
203
|
+
key_fk_object.pk = fk_object.pk
|
|
204
|
+
key_fk_object.unique = fk_object.unique
|
|
205
|
+
else:
|
|
206
|
+
key_fk_object.pk = False
|
|
207
|
+
key_fk_object.unique = False
|
|
208
|
+
model._meta.add_field(key_field, key_fk_object)
|
|
209
|
+
fk_object.related_model = related_model
|
|
210
|
+
fk_object.source_field = key_field
|
|
211
|
+
if (backward_relation_name := fk_object.related_name) is not False:
|
|
212
|
+
if not backward_relation_name:
|
|
213
|
+
backward_relation_name = f"{model._meta.db_table}s"
|
|
214
|
+
if backward_relation_name in related_model._meta.fields:
|
|
215
|
+
raise ConfigurationError(
|
|
216
|
+
f'backward relation "{backward_relation_name}" duplicates in'
|
|
217
|
+
f" model {related_model_name}"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
fk_relation = (
|
|
221
|
+
BackwardOneToOneRelation(
|
|
222
|
+
model,
|
|
223
|
+
key_field,
|
|
224
|
+
key_fk_object.source_field,
|
|
225
|
+
null=True,
|
|
226
|
+
description=fk_object.description,
|
|
227
|
+
)
|
|
228
|
+
if is_o2o
|
|
229
|
+
else BackwardFKRelation(
|
|
230
|
+
model,
|
|
231
|
+
key_field,
|
|
232
|
+
key_fk_object.source_field,
|
|
233
|
+
null=fk_object.null,
|
|
234
|
+
description=fk_object.description,
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
fk_relation.to_field_instance = fk_object.to_field_instance
|
|
238
|
+
if isinstance(backward_relation_name, str):
|
|
239
|
+
related_model._meta.add_field(backward_relation_name, fk_relation)
|
|
240
|
+
if is_o2o and fk_object.pk:
|
|
241
|
+
model._meta.pk_attr = key_field
|
|
242
|
+
|
|
243
|
+
for app_name, app in self.apps.items():
|
|
244
|
+
for model_name, model in app.items():
|
|
245
|
+
if model._meta._inited:
|
|
246
|
+
continue
|
|
247
|
+
model._meta._inited = True
|
|
248
|
+
if not model._meta.db_table or (
|
|
249
|
+
model._meta.auto_table and self._table_name_generator
|
|
250
|
+
):
|
|
251
|
+
model._meta.db_table = (
|
|
252
|
+
self._table_name_generator(model)
|
|
253
|
+
if self._table_name_generator
|
|
254
|
+
else (model.__name__.lower())
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
for field in sorted(model._meta.fk_fields):
|
|
258
|
+
init_fk_o2o_field(model, field)
|
|
259
|
+
|
|
260
|
+
for field in model._meta.o2o_fields:
|
|
261
|
+
init_fk_o2o_field(model, field, is_o2o=True)
|
|
262
|
+
|
|
263
|
+
for field in list(model._meta.m2m_fields):
|
|
264
|
+
m2m_object = cast(ManyToManyFieldInstance, model._meta.fields_map[field])
|
|
265
|
+
if m2m_object._generated:
|
|
266
|
+
continue
|
|
267
|
+
if not (backward_key := m2m_object.backward_key):
|
|
268
|
+
backward_key = f"{model._meta.db_table}_id"
|
|
269
|
+
if backward_key == m2m_object.forward_key:
|
|
270
|
+
backward_key = f"{model._meta.db_table}_rel_id"
|
|
271
|
+
m2m_object.backward_key = backward_key
|
|
272
|
+
|
|
273
|
+
reference = m2m_object.model_name
|
|
274
|
+
if not isinstance(reference, str):
|
|
275
|
+
related_model = reference
|
|
276
|
+
related_model_name = related_model.__name__
|
|
277
|
+
else:
|
|
278
|
+
related_app_name, related_model_name = split_reference(reference)
|
|
279
|
+
related_model = get_related_model(related_app_name, related_model_name)
|
|
280
|
+
|
|
281
|
+
m2m_object.related_model = related_model
|
|
282
|
+
|
|
283
|
+
if not (backward_relation_name := m2m_object.related_name):
|
|
284
|
+
backward_relation_name = m2m_object.related_name = (
|
|
285
|
+
f"{model._meta.db_table}s"
|
|
286
|
+
)
|
|
287
|
+
if backward_relation_name in related_model._meta.fields:
|
|
288
|
+
raise ConfigurationError(
|
|
289
|
+
f'backward relation "{backward_relation_name}" duplicates in'
|
|
290
|
+
f" model {related_model_name}"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
if not m2m_object.through:
|
|
294
|
+
related_model_table_name = (
|
|
295
|
+
related_model._meta.db_table or related_model.__name__.lower()
|
|
296
|
+
)
|
|
297
|
+
m2m_object.through = f"{model._meta.db_table}_{related_model_table_name}"
|
|
298
|
+
|
|
299
|
+
m2m_object.through_schema = model._meta.schema
|
|
300
|
+
|
|
301
|
+
m2m_relation = ManyToManyFieldInstance(
|
|
302
|
+
f"{app_name}.{model_name}",
|
|
303
|
+
m2m_object.through,
|
|
304
|
+
forward_key=m2m_object.backward_key,
|
|
305
|
+
backward_key=m2m_object.forward_key,
|
|
306
|
+
related_name=field,
|
|
307
|
+
field_type=model,
|
|
308
|
+
description=m2m_object.description,
|
|
309
|
+
)
|
|
310
|
+
m2m_relation._generated = True
|
|
311
|
+
m2m_relation.through_schema = model._meta.schema
|
|
312
|
+
model._meta.filters.update(get_m2m_filters(field, m2m_object))
|
|
313
|
+
related_model._meta.add_field(backward_relation_name, m2m_relation)
|
|
314
|
+
|
|
315
|
+
def get_model_reference(self, model: type[Model]) -> str:
|
|
316
|
+
return model._meta.full_name
|
|
317
|
+
|
|
318
|
+
def get_model(self, app_label: str, model_name: str) -> type[Model]:
|
|
319
|
+
try:
|
|
320
|
+
return self.apps[app_label][model_name]
|
|
321
|
+
except KeyError:
|
|
322
|
+
if app_label not in self.apps:
|
|
323
|
+
raise ConfigurationError(f"No app with name '{app_label}' registered.")
|
|
324
|
+
raise ConfigurationError(
|
|
325
|
+
f"No model with name '{model_name}' registered in app '{app_label}'."
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
def get_models_iterable(self) -> Iterable[type[Model]]:
|
|
329
|
+
model_list_generator = (
|
|
330
|
+
model_list for model_list in (app.values() for app in self.apps.values())
|
|
331
|
+
)
|
|
332
|
+
return chain.from_iterable(model_list_generator)
|
|
333
|
+
|
|
334
|
+
def clear(self) -> None:
|
|
335
|
+
self.apps.clear()
|
|
336
|
+
|
|
337
|
+
def __contains__(self, key: str) -> bool:
|
|
338
|
+
return key in self.apps
|
|
339
|
+
|
|
340
|
+
def __iter__(self) -> Iterator[str]:
|
|
341
|
+
return self.apps.__iter__()
|
|
342
|
+
|
|
343
|
+
def values(self) -> Iterable[dict[str, type[Model]]]:
|
|
344
|
+
return self.apps.values()
|
|
345
|
+
|
|
346
|
+
def items(self) -> Iterable[tuple[str, dict[str, type[Model]]]]:
|
|
347
|
+
return self.apps.items()
|
|
348
|
+
|
|
349
|
+
def keys(self) -> Iterable[str]:
|
|
350
|
+
return self.apps.keys()
|
|
351
|
+
|
|
352
|
+
def __getitem__(self, key: str) -> dict[str, type[Model]]:
|
|
353
|
+
return self.apps[key]
|
|
354
|
+
|
|
355
|
+
def __setitem__(self, key: str, value: dict[str, type[Model]]) -> None:
|
|
356
|
+
self.apps[key] = value
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# Copyright (c) 2026 ropmyung (Rakko ORM modifications and additions).
|
|
2
|
+
# Derived from Tortoise ORM; maintained and modified for Rakko ORM.
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
from collections.abc import Awaitable, Callable
|
|
9
|
+
from typing import Any, TypeVar, cast
|
|
10
|
+
|
|
11
|
+
import asyncpg
|
|
12
|
+
from asyncpg.transaction import Transaction
|
|
13
|
+
|
|
14
|
+
from rakko.backends.asyncpg.executor import AsyncpgExecutor
|
|
15
|
+
from rakko.backends.asyncpg.schema_generator import AsyncpgSchemaGenerator
|
|
16
|
+
from rakko.backends.base.client import (
|
|
17
|
+
ConnectionWrapper,
|
|
18
|
+
NestedTransactionContext,
|
|
19
|
+
TransactionalDBClient,
|
|
20
|
+
TransactionContext,
|
|
21
|
+
TransactionContextPooled,
|
|
22
|
+
)
|
|
23
|
+
from rakko.backends.base_postgres.client import (
|
|
24
|
+
BasePostgresClient,
|
|
25
|
+
translate_exceptions,
|
|
26
|
+
)
|
|
27
|
+
from rakko.exceptions import (
|
|
28
|
+
DBConnectionError,
|
|
29
|
+
IntegrityError,
|
|
30
|
+
OperationalError,
|
|
31
|
+
TransactionManagementError,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
FuncType = Callable[..., Any]
|
|
35
|
+
F = TypeVar("F", bound=FuncType)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AsyncpgDBClient(BasePostgresClient):
|
|
39
|
+
executor_class = AsyncpgExecutor
|
|
40
|
+
schema_generator = AsyncpgSchemaGenerator
|
|
41
|
+
connection_class = asyncpg.connection.Connection
|
|
42
|
+
_pool: asyncpg.Pool | None
|
|
43
|
+
_connection: asyncpg.connection.Connection | None = None
|
|
44
|
+
|
|
45
|
+
async def create_connection(self, with_db: bool) -> None:
|
|
46
|
+
if self.schema:
|
|
47
|
+
self.server_settings["search_path"] = self.schema
|
|
48
|
+
|
|
49
|
+
if self.application_name:
|
|
50
|
+
self.server_settings["application_name"] = self.application_name
|
|
51
|
+
|
|
52
|
+
self._template = {
|
|
53
|
+
"host": self.host,
|
|
54
|
+
"port": self.port,
|
|
55
|
+
"user": self.user,
|
|
56
|
+
"database": self.database if with_db else None,
|
|
57
|
+
"min_size": self.pool_minsize,
|
|
58
|
+
"max_size": self.pool_maxsize,
|
|
59
|
+
"connection_class": self.connection_class,
|
|
60
|
+
"loop": self.loop,
|
|
61
|
+
"server_settings": self.server_settings,
|
|
62
|
+
**self.extra,
|
|
63
|
+
}
|
|
64
|
+
try:
|
|
65
|
+
# asyncpg resolves a callable password itself, once per new connection
|
|
66
|
+
self._pool = await self.create_pool(password=self.password, **self._template)
|
|
67
|
+
await self._post_connect()
|
|
68
|
+
self.log.debug("Created connection pool %s with params: %s", self._pool, self._template)
|
|
69
|
+
except asyncpg.InvalidCatalogNameError as ex:
|
|
70
|
+
msg = "Can't establish connection to "
|
|
71
|
+
if with_db:
|
|
72
|
+
msg += f"database {self.database}"
|
|
73
|
+
else:
|
|
74
|
+
msg += f"default database. Verify environment PGDATABASE. Exception: {ex}"
|
|
75
|
+
raise DBConnectionError(msg) from ex
|
|
76
|
+
|
|
77
|
+
async def create_pool(self, **kwargs) -> asyncpg.Pool:
|
|
78
|
+
return await asyncpg.create_pool(None, **kwargs)
|
|
79
|
+
|
|
80
|
+
async def _expire_connections(self) -> None:
|
|
81
|
+
if self._pool: # pragma: nobranch
|
|
82
|
+
await self._pool.expire_connections()
|
|
83
|
+
|
|
84
|
+
async def _close(self) -> None:
|
|
85
|
+
if self._pool: # pragma: nobranch
|
|
86
|
+
try:
|
|
87
|
+
await asyncio.wait_for(self._pool.close(), 10)
|
|
88
|
+
except asyncio.TimeoutError: # pragma: nocoverage
|
|
89
|
+
self._pool.terminate()
|
|
90
|
+
self._pool = None
|
|
91
|
+
self.log.debug("Closed connection pool %s with params: %s", self._pool, self._template)
|
|
92
|
+
|
|
93
|
+
async def _translate_exceptions[R](
|
|
94
|
+
self, func: Callable[..., Awaitable[R]], *args: Any, **kwargs: Any
|
|
95
|
+
) -> R:
|
|
96
|
+
try:
|
|
97
|
+
return await func(self, *args, **kwargs)
|
|
98
|
+
except (asyncpg.SyntaxOrAccessError, asyncpg.exceptions.DataError) as exc:
|
|
99
|
+
raise OperationalError(exc) from exc
|
|
100
|
+
except asyncpg.IntegrityConstraintViolationError as exc:
|
|
101
|
+
raise IntegrityError(exc) from exc
|
|
102
|
+
except asyncpg.InvalidTransactionStateError as exc: # pragma: nocoverage
|
|
103
|
+
raise TransactionManagementError(exc) from exc
|
|
104
|
+
|
|
105
|
+
async def db_delete(self) -> None:
|
|
106
|
+
try:
|
|
107
|
+
return await super().db_delete()
|
|
108
|
+
except asyncpg.InvalidCatalogNameError: # pragma: nocoverage
|
|
109
|
+
pass
|
|
110
|
+
await self.close()
|
|
111
|
+
|
|
112
|
+
def _in_transaction(self) -> TransactionContext:
|
|
113
|
+
return TransactionContextPooled(TransactionWrapper(self), self._pool_init_lock)
|
|
114
|
+
|
|
115
|
+
@translate_exceptions
|
|
116
|
+
async def execute_insert(self, query: str, values: list) -> asyncpg.Record | None:
|
|
117
|
+
async with self.acquire_connection() as connection:
|
|
118
|
+
self.log.debug("%s: %s", query, values)
|
|
119
|
+
# TODO: Cache prepared statement
|
|
120
|
+
return await connection.fetchrow(query, *values)
|
|
121
|
+
|
|
122
|
+
@translate_exceptions
|
|
123
|
+
async def execute_many(self, query: str, values: list) -> None:
|
|
124
|
+
async with self.acquire_connection() as connection:
|
|
125
|
+
self.log.debug("%s: %s", query, values)
|
|
126
|
+
# TODO: Consider using copy_records_to_table instead
|
|
127
|
+
transaction = connection.transaction()
|
|
128
|
+
await transaction.start()
|
|
129
|
+
try:
|
|
130
|
+
await connection.executemany(query, values)
|
|
131
|
+
except Exception:
|
|
132
|
+
await transaction.rollback()
|
|
133
|
+
raise
|
|
134
|
+
else:
|
|
135
|
+
await transaction.commit()
|
|
136
|
+
|
|
137
|
+
@translate_exceptions
|
|
138
|
+
async def execute_query(self, query: str, values: list | None = None) -> tuple[int, list[dict]]:
|
|
139
|
+
async with self.acquire_connection() as connection:
|
|
140
|
+
self.log.debug("%s: %s", query, values)
|
|
141
|
+
params = [query, *values] if values else [query]
|
|
142
|
+
normalized = query.lstrip().upper()
|
|
143
|
+
if normalized.startswith(("UPDATE", "DELETE", "INSERT")):
|
|
144
|
+
res = await connection.execute(*params)
|
|
145
|
+
try:
|
|
146
|
+
rows_affected = int(res.split(" ")[1])
|
|
147
|
+
except (AttributeError, IndexError, ValueError): # pragma: nocoverage
|
|
148
|
+
rows_affected = 0
|
|
149
|
+
return rows_affected, []
|
|
150
|
+
|
|
151
|
+
rows = await connection.fetch(*params)
|
|
152
|
+
return len(rows), rows
|
|
153
|
+
|
|
154
|
+
@translate_exceptions
|
|
155
|
+
async def execute_query_dict(self, query: str, values: list | None = None) -> list[dict]:
|
|
156
|
+
async with self.acquire_connection() as connection:
|
|
157
|
+
self.log.debug("%s: %s", query, values)
|
|
158
|
+
params = [query, *values] if values else [query]
|
|
159
|
+
results = await connection.fetch(*params)
|
|
160
|
+
return [dict(i) for i in results]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class TransactionWrapper(AsyncpgDBClient, TransactionalDBClient):
|
|
164
|
+
"""A transactional connection wrapper for asyncpg.
|
|
165
|
+
|
|
166
|
+
asyncpg implements nested transactions (savepoints) natively, so we don't need to.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
_connection: asyncpg.Connection # Put it here to improve type hints
|
|
170
|
+
|
|
171
|
+
def __init__(self, connection: AsyncpgDBClient) -> None:
|
|
172
|
+
self._connection = cast(asyncpg.Connection, connection._connection) # type: ignore[assignment]
|
|
173
|
+
self._lock = asyncio.Lock()
|
|
174
|
+
self.log = connection.log
|
|
175
|
+
self.connection_name = connection.connection_name
|
|
176
|
+
self.transaction: Transaction | None = None
|
|
177
|
+
self._finalized = False
|
|
178
|
+
self._parent: AsyncpgDBClient = connection # type: ignore[assignment]
|
|
179
|
+
|
|
180
|
+
def _in_transaction(self) -> TransactionContext:
|
|
181
|
+
# since we need to store the transaction object for each transaction block,
|
|
182
|
+
# we need to wrap the connection with its own TransactionWrapper
|
|
183
|
+
return NestedTransactionContext(TransactionWrapper(self))
|
|
184
|
+
|
|
185
|
+
def acquire_connection(self) -> ConnectionWrapper[asyncpg.Connection]:
|
|
186
|
+
return ConnectionWrapper(self._lock, self)
|
|
187
|
+
|
|
188
|
+
@translate_exceptions
|
|
189
|
+
async def execute_many(self, query: str, values: list) -> None:
|
|
190
|
+
async with self.acquire_connection() as connection:
|
|
191
|
+
self.log.debug("%s: %s", query, values)
|
|
192
|
+
# TODO: Consider using copy_records_to_table instead
|
|
193
|
+
await connection.executemany(query, values)
|
|
194
|
+
|
|
195
|
+
@translate_exceptions
|
|
196
|
+
async def begin(self) -> None:
|
|
197
|
+
self.transaction = self._connection.transaction()
|
|
198
|
+
await self.transaction.start()
|
|
199
|
+
|
|
200
|
+
async def savepoint(self) -> None:
|
|
201
|
+
return await self.begin()
|
|
202
|
+
|
|
203
|
+
async def commit(self) -> None:
|
|
204
|
+
if not self.transaction:
|
|
205
|
+
raise TransactionManagementError("Transaction is in invalid state")
|
|
206
|
+
if self._finalized:
|
|
207
|
+
raise TransactionManagementError("Transaction already finalised")
|
|
208
|
+
await self.transaction.commit()
|
|
209
|
+
self._finalized = True
|
|
210
|
+
|
|
211
|
+
async def release_savepoint(self) -> None:
|
|
212
|
+
return await self.commit()
|
|
213
|
+
|
|
214
|
+
async def rollback(self) -> None:
|
|
215
|
+
if not self.transaction:
|
|
216
|
+
raise TransactionManagementError("Transaction is in invalid state")
|
|
217
|
+
if self._finalized:
|
|
218
|
+
raise TransactionManagementError("Transaction already finalised")
|
|
219
|
+
await self.transaction.rollback()
|
|
220
|
+
self._finalized = True
|
|
221
|
+
|
|
222
|
+
async def savepoint_rollback(self) -> None:
|
|
223
|
+
await self.rollback()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Copyright (c) 2026 ropmyung (Rakko ORM modifications and additions).
|
|
2
|
+
# Derived from Tortoise ORM; maintained and modified for Rakko ORM.
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
|
|
10
|
+
import asyncpg
|
|
11
|
+
|
|
12
|
+
from rakko import Model
|
|
13
|
+
from rakko.backends.base_postgres.executor import BasePostgresExecutor
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AsyncpgExecutor(BasePostgresExecutor):
|
|
17
|
+
async def _process_insert_result(
|
|
18
|
+
self, instance: Model, results: asyncpg.Record | Mapping[str, Any] | None
|
|
19
|
+
) -> None:
|
|
20
|
+
return await super()._process_insert_result(instance, cast(Mapping[str, Any] | None, results))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Copyright (c) 2026 ropmyung (Rakko ORM modifications and additions).
|
|
2
|
+
# Derived from Tortoise ORM; maintained and modified for Rakko ORM.
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from rakko.backends.base_postgres.schema_generator import BasePostgresSchemaGenerator
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING: # pragma: nocoverage
|
|
12
|
+
from rakko.backends.asyncpg.client import AsyncpgDBClient
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AsyncpgSchemaGenerator(BasePostgresSchemaGenerator):
|
|
16
|
+
def __init__(self, client: AsyncpgDBClient) -> None:
|
|
17
|
+
super().__init__(client)
|