rb-commons 0.4.13__py3-none-any.whl → 0.5.0__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.
- rb_commons/orm/managers.py +236 -336
- {rb_commons-0.4.13.dist-info → rb_commons-0.5.0.dist-info}/METADATA +1 -1
- {rb_commons-0.4.13.dist-info → rb_commons-0.5.0.dist-info}/RECORD +5 -5
- {rb_commons-0.4.13.dist-info → rb_commons-0.5.0.dist-info}/WHEEL +0 -0
- {rb_commons-0.4.13.dist-info → rb_commons-0.5.0.dist-info}/top_level.txt +0 -0
rb_commons/orm/managers.py
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import uuid
|
2
|
-
from typing import TypeVar, Type, Generic, Optional, List, Dict, Literal, Union, Sequence
|
3
|
-
from sqlalchemy import select, delete, update, and_, func, desc, inspect
|
4
|
+
from typing import TypeVar, Type, Generic, Optional, List, Dict, Literal, Union, Sequence, Any, Iterable
|
5
|
+
from sqlalchemy import select, delete, update, and_, func, desc, inspect, or_
|
4
6
|
from sqlalchemy.exc import IntegrityError, SQLAlchemyError, NoResultFound
|
5
7
|
from sqlalchemy.ext.asyncio import AsyncSession
|
6
8
|
from sqlalchemy.orm import declarative_base, InstrumentedAttribute, selectinload, RelationshipProperty
|
@@ -10,6 +12,46 @@ from rb_commons.orm.exceptions import DatabaseException, InternalException
|
|
10
12
|
|
11
13
|
ModelType = TypeVar('ModelType', bound=declarative_base())
|
12
14
|
|
15
|
+
class Q:
|
16
|
+
"""Boolean logic container that can be combined with `&`, `|`, and `~`."""
|
17
|
+
|
18
|
+
def __init__(self, **lookups: Any) -> None:
|
19
|
+
self.lookups: Dict[str, Any] = lookups
|
20
|
+
self.children: List[Q] = []
|
21
|
+
self._operator: str = "AND"
|
22
|
+
self.negated: bool = False
|
23
|
+
|
24
|
+
def _combine(self, other: "Q", operator: str) -> "Q":
|
25
|
+
combined = Q()
|
26
|
+
combined.children = [self, other]
|
27
|
+
combined._operator = operator
|
28
|
+
return combined
|
29
|
+
|
30
|
+
def __or__(self, other: "Q") -> "Q":
|
31
|
+
return self._combine(other, "OR")
|
32
|
+
|
33
|
+
def __and__(self, other: "Q") -> "Q":
|
34
|
+
return self._combine(other, "AND")
|
35
|
+
|
36
|
+
def __invert__(self) -> "Q":
|
37
|
+
clone = Q()
|
38
|
+
clone.lookups = self.lookups.copy()
|
39
|
+
clone.children = list(self.children)
|
40
|
+
clone._operator = self._operator
|
41
|
+
clone.negated = not self.negated
|
42
|
+
return clone
|
43
|
+
|
44
|
+
def __repr__(self) -> str:
|
45
|
+
if self.lookups:
|
46
|
+
base = f"Q({self.lookups})"
|
47
|
+
else:
|
48
|
+
base = "Q()"
|
49
|
+
if self.children:
|
50
|
+
base += f" {self._operator} {self.children}"
|
51
|
+
if self.negated:
|
52
|
+
base = f"NOT({base})"
|
53
|
+
return base
|
54
|
+
|
13
55
|
def with_transaction_error_handling(func):
|
14
56
|
async def wrapper(self, *args, **kwargs):
|
15
57
|
try:
|
@@ -29,435 +71,293 @@ class BaseManager(Generic[ModelType]):
|
|
29
71
|
model: Type[ModelType]
|
30
72
|
|
31
73
|
def __init__(self, session: AsyncSession) -> None:
|
32
|
-
self.session = session
|
33
|
-
self.
|
34
|
-
self.
|
35
|
-
self.
|
36
|
-
self._limit = None
|
74
|
+
self.session: AsyncSession = session
|
75
|
+
self.filters: List[Any] = []
|
76
|
+
self._filtered: bool = False
|
77
|
+
self._limit: Optional[int] = None
|
37
78
|
|
38
79
|
async def _smart_commit(self, instance: Optional[ModelType] = None) -> Optional[ModelType]:
|
39
80
|
if not self.session.in_transaction():
|
40
81
|
await self.session.commit()
|
41
|
-
|
42
|
-
if instance:
|
82
|
+
if instance is not None:
|
43
83
|
await self.session.refresh(instance)
|
44
84
|
return instance
|
85
|
+
return None
|
86
|
+
|
87
|
+
def _parse_lookup(self, lookup: str, value: Any):
|
88
|
+
parts = lookup.split("__")
|
89
|
+
operator = "eq"
|
90
|
+
if parts[-1] in {"eq", "ne", "gt", "lt", "gte", "lte", "in", "contains", "null"}:
|
91
|
+
operator = parts.pop()
|
92
|
+
|
93
|
+
current: Union[Type[ModelType], InstrumentedAttribute] = self.model
|
94
|
+
# walk relationships
|
95
|
+
for field in parts:
|
96
|
+
attr = getattr(current, field, None)
|
97
|
+
if attr is None:
|
98
|
+
raise ValueError(f"Invalid filter field: {'.'.join(parts)}")
|
99
|
+
if hasattr(attr, "property") and isinstance(attr.property, RelationshipProperty):
|
100
|
+
current = attr.property.mapper.class_
|
101
|
+
else:
|
102
|
+
current = attr
|
103
|
+
|
104
|
+
if operator == "eq":
|
105
|
+
return current == value
|
106
|
+
if operator == "ne":
|
107
|
+
return current != value
|
108
|
+
if operator == "gt":
|
109
|
+
return current > value
|
110
|
+
if operator == "lt":
|
111
|
+
return current < value
|
112
|
+
if operator == "gte":
|
113
|
+
return current >= value
|
114
|
+
if operator == "lte":
|
115
|
+
return current <= value
|
116
|
+
if operator == "in":
|
117
|
+
if not isinstance(value, (list, tuple, set)):
|
118
|
+
raise ValueError(f"{lookup}__in requires an iterable")
|
119
|
+
return current.in_(value)
|
120
|
+
if operator == "contains":
|
121
|
+
return current.ilike(f"%{value}%")
|
122
|
+
if operator == "null":
|
123
|
+
return current.is_(None) if value else current.isnot(None)
|
124
|
+
raise ValueError(f"Unsupported operator in lookup: {lookup}")
|
125
|
+
|
126
|
+
def _q_to_expr(self, q: Q):
|
127
|
+
clauses: List[Any] = [self._parse_lookup(k, v) for k, v in q.lookups.items()]
|
128
|
+
for child in q.children:
|
129
|
+
clauses.append(self._q_to_expr(child))
|
130
|
+
combined = (
|
131
|
+
True
|
132
|
+
if not clauses
|
133
|
+
else (or_(*clauses) if q._operator == "OR" else and_(*clauses))
|
134
|
+
)
|
135
|
+
return ~combined if q.negated else combined
|
45
136
|
|
46
|
-
|
47
|
-
"""
|
48
|
-
get object based on conditions
|
49
|
-
"""
|
50
|
-
stmt = select(self.model).filter_by(id=pk)
|
137
|
+
def filter(self, *expressions: Any, **lookups: Any) -> "BaseManager[ModelType]":
|
138
|
+
"""Add **AND** constraints (default behaviour).
|
51
139
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
140
|
+
* `expressions` can be raw SQLAlchemy clauses **or** `Q` objects.
|
141
|
+
* `lookups` are Django‑style keyword filters.
|
142
|
+
"""
|
143
|
+
self._filtered = True
|
144
|
+
for expr in expressions:
|
145
|
+
self.filters.append(self._q_to_expr(expr) if isinstance(expr, Q) else expr)
|
146
|
+
for k, v in lookups.items():
|
147
|
+
self.filters.append(self._parse_lookup(k, v))
|
148
|
+
return self
|
58
149
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
150
|
+
def or_filter(self, *expressions: Any, **lookups: Any) -> "BaseManager[ModelType]":
|
151
|
+
"""Add one OR group (shortcut for `filter(Q() | Q())`)."""
|
152
|
+
or_clauses: List[Any] = []
|
153
|
+
for expr in expressions:
|
154
|
+
or_clauses.append(self._q_to_expr(expr) if isinstance(expr, Q) else expr)
|
155
|
+
for k, v in lookups.items():
|
156
|
+
or_clauses.append(self._parse_lookup(k, v))
|
157
|
+
if or_clauses:
|
158
|
+
self._filtered = True
|
159
|
+
self.filters.append(or_(*or_clauses))
|
160
|
+
return self
|
65
161
|
|
66
|
-
|
162
|
+
def limit(self, value: int) -> "BaseManager[ModelType]":
|
163
|
+
self._limit = value
|
164
|
+
return self
|
67
165
|
|
68
166
|
def _apply_eager_loading(self, stmt, load_all_relations: bool = False):
|
69
167
|
if not load_all_relations:
|
70
168
|
return stmt
|
71
|
-
|
72
|
-
|
73
|
-
visited = set()
|
169
|
+
opts: List[Any] = []
|
170
|
+
visited: set[Any] = set()
|
74
171
|
|
75
172
|
def recurse(model, loader=None):
|
76
173
|
mapper = inspect(model)
|
77
174
|
if mapper in visited:
|
78
175
|
return
|
79
176
|
visited.add(mapper)
|
80
|
-
|
81
177
|
for rel in mapper.relationships:
|
82
178
|
attr = getattr(model, rel.key)
|
83
|
-
if loader is None
|
84
|
-
this_loader = selectinload(attr)
|
85
|
-
else:
|
86
|
-
this_loader = loader.selectinload(attr)
|
179
|
+
this_loader = selectinload(attr) if loader is None else loader.selectinload(attr)
|
87
180
|
opts.append(this_loader)
|
88
181
|
recurse(rel.mapper.class_, this_loader)
|
89
182
|
|
90
183
|
recurse(self.model)
|
91
184
|
return stmt.options(*opts)
|
92
185
|
|
93
|
-
def
|
94
|
-
"""
|
95
|
-
Dynamically apply filters to the query.
|
96
|
-
|
97
|
-
Supported operators:
|
98
|
-
- __eq (e.g., field__eq=value) or just field=value
|
99
|
-
- __ne (field__ne=value)
|
100
|
-
- __gt (field__gt=value)
|
101
|
-
- __lt (field__lt=value)
|
102
|
-
- __gte (field__gte=value)
|
103
|
-
- __lte (field__lte=value)
|
104
|
-
- __in (field__in=[val1, val2, ...])
|
105
|
-
- __contains (field__contains='text')
|
106
|
-
- __null (field__null=True/False) - True for IS NULL, False for IS NOT NULL
|
107
|
-
|
108
|
-
Additionally supports nested paths, e.g.,
|
109
|
-
product__shop_id=None
|
110
|
-
product__shop__country='US'
|
111
|
-
"""
|
112
|
-
self._filtered = True
|
113
|
-
self.filters = []
|
114
|
-
|
115
|
-
for key, value in kwargs.items():
|
116
|
-
parts = key.split("__")
|
117
|
-
|
118
|
-
operator = "eq"
|
119
|
-
|
120
|
-
if parts[-1] in {"eq", "ne", "gt", "lt", "gte", "lte", "in", "contains", "null"}:
|
121
|
-
operator = parts.pop()
|
122
|
-
|
123
|
-
current_attr = self.model
|
124
|
-
|
125
|
-
for field_name in parts:
|
126
|
-
attr_candidate = getattr(current_attr, field_name, None)
|
127
|
-
if attr_candidate is None:
|
128
|
-
raise ValueError(f"Invalid filter field: {'.'.join(parts)}")
|
129
|
-
|
130
|
-
if hasattr(attr_candidate, "property") and isinstance(attr_candidate.property, RelationshipProperty):
|
131
|
-
current_attr = attr_candidate.property.mapper.class_
|
132
|
-
else:
|
133
|
-
current_attr = attr_candidate
|
134
|
-
|
135
|
-
if operator == "eq":
|
136
|
-
# e.g., column == value
|
137
|
-
self.filters.append(current_attr == value)
|
138
|
-
|
139
|
-
elif operator == "ne":
|
140
|
-
# e.g., column != value
|
141
|
-
self.filters.append(current_attr != value)
|
142
|
-
|
143
|
-
elif operator == "gt":
|
144
|
-
self.filters.append(current_attr > value)
|
145
|
-
|
146
|
-
elif operator == "lt":
|
147
|
-
self.filters.append(current_attr < value)
|
148
|
-
|
149
|
-
elif operator == "gte":
|
150
|
-
self.filters.append(current_attr >= value)
|
151
|
-
|
152
|
-
elif operator == "lte":
|
153
|
-
self.filters.append(current_attr <= value)
|
154
|
-
|
155
|
-
elif operator == "in":
|
156
|
-
if not isinstance(value, list):
|
157
|
-
raise ValueError(f"{'.'.join(parts)}__in requires a list, got {type(value)}")
|
158
|
-
self.filters.append(current_attr.in_(value))
|
159
|
-
|
160
|
-
elif operator == "contains":
|
161
|
-
# e.g., column ILIKE %value%
|
162
|
-
self.filters.append(current_attr.ilike(f"%{value}%"))
|
163
|
-
|
164
|
-
elif operator == "null":
|
165
|
-
if value is True:
|
166
|
-
self.filters.append(current_attr.is_(None))
|
167
|
-
else:
|
168
|
-
self.filters.append(current_attr.isnot(None))
|
169
|
-
|
170
|
-
return self
|
171
|
-
|
172
|
-
def _ensure_filtered(self):
|
173
|
-
"""Ensure that `filter()` has been called before using certain methods."""
|
174
|
-
if not self._filtered:
|
175
|
-
raise RuntimeError("You must call `filter()` before using this method.")
|
176
|
-
|
177
|
-
async def _execute_query_and_unique_data(self, stmt, load_all_relations: bool) -> List[ModelType]:
|
186
|
+
async def _execute_query(self, stmt, load_all_relations: bool):
|
178
187
|
stmt = self._apply_eager_loading(stmt, load_all_relations)
|
179
188
|
result = await self.session.execute(stmt)
|
180
189
|
rows = result.scalars().all()
|
181
|
-
|
182
|
-
return list(unique_by_pk.values())
|
183
|
-
|
184
|
-
async def all(self, load_all_relations: bool = False) -> List[ModelType] | None:
|
185
|
-
try:
|
186
|
-
stmt = select(self.model)
|
187
|
-
|
188
|
-
if self._filtered:
|
189
|
-
stmt = stmt.filter(and_(*self.filters))
|
190
|
-
|
191
|
-
if self._limit:
|
192
|
-
stmt = stmt.limit(self._limit)
|
190
|
+
return list({obj.id: obj for obj in rows}.values())
|
193
191
|
|
194
|
-
|
195
|
-
|
196
|
-
return await self._execute_query_and_unique_data(stmt, load_all_relations)
|
197
|
-
finally:
|
198
|
-
self._clear_query_state()
|
199
|
-
|
200
|
-
def _clear_query_state(self):
|
201
|
-
"""Clear all query state after execution"""
|
192
|
+
def _reset_state(self):
|
193
|
+
self.filters.clear()
|
202
194
|
self._filtered = False
|
203
|
-
self.filters = []
|
204
195
|
self._limit = None
|
205
196
|
|
206
|
-
async def
|
207
|
-
self.
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
return self
|
197
|
+
async def all(self, load_all_relations: bool = False):
|
198
|
+
stmt = select(self.model)
|
199
|
+
if self.filters:
|
200
|
+
stmt = stmt.filter(and_(*self.filters))
|
201
|
+
if self._limit:
|
202
|
+
stmt = stmt.limit(self._limit)
|
203
|
+
try:
|
204
|
+
return await self._execute_query(stmt, load_all_relations)
|
205
|
+
finally:
|
206
|
+
self._reset_state()
|
217
207
|
|
218
|
-
async def first(self, load_relations: Sequence[str] = None)
|
219
|
-
"""Return the first matching object, or None."""
|
208
|
+
async def first(self, load_relations: Optional[Sequence[str]] = None):
|
220
209
|
self._ensure_filtered()
|
221
|
-
|
222
210
|
stmt = select(self.model).filter(and_(*self.filters))
|
223
|
-
|
224
211
|
if load_relations:
|
225
212
|
for rel in load_relations:
|
226
213
|
stmt = stmt.options(selectinload(getattr(self.model, rel)))
|
227
|
-
|
228
214
|
result = await self.session.execute(stmt)
|
215
|
+
self._reset_state()
|
229
216
|
return result.scalars().first()
|
230
217
|
|
231
|
-
async def last(self, load_relations: Sequence[str] = None)
|
232
|
-
"""Return the last matching object, or None."""
|
233
|
-
|
218
|
+
async def last(self, load_relations: Optional[Sequence[str]] = None):
|
234
219
|
self._ensure_filtered()
|
235
|
-
|
236
|
-
|
237
|
-
|
220
|
+
stmt = (
|
221
|
+
select(self.model)
|
222
|
+
.filter(and_(*self.filters))
|
223
|
+
.order_by(desc(self.model.id))
|
224
|
+
)
|
238
225
|
if load_relations:
|
239
226
|
for rel in load_relations:
|
240
227
|
stmt = stmt.options(selectinload(getattr(self.model, rel)))
|
241
|
-
|
242
228
|
result = await self.session.execute(stmt)
|
229
|
+
self._reset_state()
|
243
230
|
return result.scalars().first()
|
244
231
|
|
245
232
|
async def count(self) -> int:
|
246
|
-
"""Return the count of matching records."""
|
247
|
-
|
248
233
|
self._ensure_filtered()
|
249
234
|
query = select(func.count()).select_from(self.model).filter(and_(*self.filters))
|
250
235
|
result = await self.session.execute(query)
|
236
|
+
self._reset_state()
|
251
237
|
return result.scalar_one()
|
252
238
|
|
239
|
+
async def paginate(
|
240
|
+
self,
|
241
|
+
limit: int = 10,
|
242
|
+
offset: int = 0,
|
243
|
+
load_all_relations: bool = False,
|
244
|
+
):
|
245
|
+
self._ensure_filtered()
|
246
|
+
stmt = (
|
247
|
+
select(self.model)
|
248
|
+
.filter(and_(*self.filters))
|
249
|
+
.limit(limit)
|
250
|
+
.offset(offset)
|
251
|
+
)
|
252
|
+
try:
|
253
|
+
return await self._execute_query(stmt, load_all_relations)
|
254
|
+
finally:
|
255
|
+
self._reset_state()
|
256
|
+
|
253
257
|
@with_transaction_error_handling
|
254
|
-
async def create(self, **kwargs)
|
255
|
-
"""
|
256
|
-
Create a new object
|
257
|
-
"""
|
258
|
+
async def create(self, **kwargs):
|
258
259
|
obj = self.model(**kwargs)
|
259
|
-
|
260
260
|
self.session.add(obj)
|
261
261
|
await self.session.flush()
|
262
262
|
return await self._smart_commit(obj)
|
263
263
|
|
264
264
|
@with_transaction_error_handling
|
265
|
-
async def
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
- If `instance` is not provided, delete according to self.filters.
|
270
|
-
|
271
|
-
:arg instance: Model instance to delete.
|
272
|
-
:return: Number of deleted records or None
|
273
|
-
"""
|
274
|
-
|
275
|
-
if instance is not None:
|
276
|
-
await self.session.delete(instance)
|
277
|
-
await self.session.commit()
|
278
|
-
return True
|
279
|
-
|
280
|
-
self._ensure_filtered()
|
281
|
-
|
282
|
-
try:
|
283
|
-
delete_stmt = delete(self.model).where(and_(*self.filters))
|
284
|
-
await self.session.execute(delete_stmt)
|
285
|
-
await self.session.commit()
|
286
|
-
return True
|
287
|
-
except NoResultFound:
|
288
|
-
return False
|
289
|
-
|
290
|
-
@with_transaction_error_handling
|
291
|
-
async def bulk_delete(self) -> int:
|
292
|
-
"""
|
293
|
-
Bulk delete with flexible filtering.
|
294
|
-
|
295
|
-
Automatically commits if not inside a transaction.
|
296
|
-
|
297
|
-
:return: Number of deleted records
|
298
|
-
"""
|
299
|
-
self._ensure_filtered()
|
300
|
-
|
301
|
-
delete_stmt = delete(self.model).where(and_(*self.filters))
|
302
|
-
result = await self.session.execute(delete_stmt)
|
303
|
-
await self._smart_commit()
|
304
|
-
return result.rowcount # ignore
|
305
|
-
|
306
|
-
@with_transaction_error_handling
|
307
|
-
async def update_by_filters(self, filters: Dict, **update_fields) -> Optional[ModelType]:
|
308
|
-
"""
|
309
|
-
Update object(s) with flexible filtering options
|
310
|
-
|
311
|
-
:param filters: Conditions for selecting records to update
|
312
|
-
:param update_fields: Fields and values to update
|
313
|
-
:return: Number of updated records
|
314
|
-
"""
|
315
|
-
if not update_fields:
|
316
|
-
raise InternalException("No fields provided for update")
|
317
|
-
|
318
|
-
update_stmt = update(self.model).filter_by(**filters).values(**update_fields)
|
319
|
-
await self.session.execute(update_stmt)
|
320
|
-
await self.session.commit()
|
321
|
-
updated_instance = await self.get(**filters)
|
322
|
-
return updated_instance
|
265
|
+
async def save(self, instance: ModelType):
|
266
|
+
self.session.add(instance)
|
267
|
+
await self.session.flush()
|
268
|
+
return await self._smart_commit(instance)
|
323
269
|
|
324
270
|
@with_transaction_error_handling
|
325
|
-
async def update(self, instance: ModelType, **
|
326
|
-
|
327
|
-
Update an existing database instance with new fields
|
328
|
-
|
329
|
-
:param instance: The database model instance to update
|
330
|
-
:param update_fields: Keyword arguments of fields to update
|
331
|
-
:return: The updated instance
|
332
|
-
|
333
|
-
:raises InternalException: If integrity violation
|
334
|
-
:raises DatabaseException: For database-related errors
|
335
|
-
"""
|
336
|
-
# Validate update fields
|
337
|
-
if not update_fields:
|
271
|
+
async def update(self, instance: ModelType, **fields):
|
272
|
+
if not fields:
|
338
273
|
raise InternalException("No fields provided for update")
|
339
|
-
|
340
|
-
|
341
|
-
for key, value in update_fields.items():
|
342
|
-
setattr(instance, key, value)
|
343
|
-
|
274
|
+
for k, v in fields.items():
|
275
|
+
setattr(instance, k, v)
|
344
276
|
self.session.add(instance)
|
345
277
|
await self._smart_commit()
|
346
|
-
|
347
278
|
return instance
|
348
279
|
|
349
280
|
@with_transaction_error_handling
|
350
|
-
async def
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
Automatically commits if not inside a transaction.
|
358
|
-
|
359
|
-
:raises InternalException: If integrity violation
|
360
|
-
:raises DatabaseException: For database-related errors
|
361
|
-
"""
|
362
|
-
self.session.add(instance)
|
363
|
-
await self.session.flush()
|
364
|
-
return await self._smart_commit(instance)
|
281
|
+
async def update_by_filters(self, filters: Dict[str, Any], **fields):
|
282
|
+
if not fields:
|
283
|
+
raise InternalException("No fields provided for update")
|
284
|
+
stmt = update(self.model).filter_by(**filters).values(**fields)
|
285
|
+
await self.session.execute(stmt)
|
286
|
+
await self.session.commit()
|
287
|
+
return await self.get(**filters)
|
365
288
|
|
366
289
|
@with_transaction_error_handling
|
367
|
-
async def
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
stmt = select(self.model).filter_by(id=instance.id)
|
380
|
-
|
381
|
-
for rel in load_relations:
|
382
|
-
stmt = stmt.options(selectinload(getattr(self.model, rel)))
|
383
|
-
|
384
|
-
result = await self.session.execute(stmt)
|
385
|
-
loaded_instance = result.scalar_one_or_none()
|
386
|
-
|
387
|
-
if loaded_instance is None:
|
388
|
-
raise NotFoundException(
|
389
|
-
message="Object saved but could not be retrieved with relationships",
|
390
|
-
status=404,
|
391
|
-
code="0001",
|
392
|
-
)
|
393
|
-
|
394
|
-
return loaded_instance
|
395
|
-
|
396
|
-
async def is_exists(self, **kwargs) -> bool:
|
397
|
-
stmt = select(self.model).filter_by(**kwargs)
|
398
|
-
result = await self.session.execute(stmt)
|
399
|
-
return result.scalar_one_or_none() is not None
|
290
|
+
async def delete(self, instance: Optional[ModelType] = None):
|
291
|
+
if instance is not None:
|
292
|
+
await self.session.delete(instance)
|
293
|
+
await self.session.commit()
|
294
|
+
return True
|
295
|
+
self._ensure_filtered()
|
296
|
+
stmt = delete(self.model).where(and_(*self.filters))
|
297
|
+
await self.session.execute(stmt)
|
298
|
+
await self.session.commit()
|
299
|
+
self._reset_state()
|
300
|
+
return True
|
400
301
|
|
401
302
|
@with_transaction_error_handling
|
402
|
-
async def bulk_save(self, instances:
|
403
|
-
"""
|
404
|
-
Bulk save a list of instances into the database.
|
405
|
-
|
406
|
-
If inside a transaction, flushes only.
|
407
|
-
If not in a transaction, commits after flush.
|
408
|
-
|
409
|
-
:param instances: List of instances
|
410
|
-
:raises DatabaseException: If any database error occurs
|
411
|
-
:raises InternalException: If any unexpected error occurs
|
412
|
-
"""
|
303
|
+
async def bulk_save(self, instances: Iterable[ModelType]):
|
413
304
|
if not instances:
|
414
305
|
return
|
415
|
-
|
416
|
-
self.session.add_all(instances)
|
306
|
+
self.session.add_all(list(instances))
|
417
307
|
await self.session.flush()
|
418
|
-
|
419
308
|
if not self.session.in_transaction():
|
420
309
|
await self.session.commit()
|
421
310
|
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
311
|
+
@with_transaction_error_handling
|
312
|
+
async def bulk_delete(self):
|
313
|
+
self._ensure_filtered()
|
314
|
+
stmt = delete(self.model).where(and_(*self.filters))
|
315
|
+
result = await self.session.execute(stmt)
|
316
|
+
await self._smart_commit()
|
317
|
+
self._reset_state()
|
318
|
+
return result.rowcount
|
319
|
+
|
320
|
+
async def get(
|
321
|
+
self,
|
322
|
+
pk: Union[str, int, uuid.UUID],
|
323
|
+
load_relations: Optional[Sequence[str]] = None,
|
324
|
+
):
|
325
|
+
stmt = select(self.model).filter_by(id=pk)
|
326
|
+
if load_relations:
|
327
|
+
for rel in load_relations:
|
328
|
+
stmt = stmt.options(selectinload(getattr(self.model, rel)))
|
329
|
+
result = await self.session.execute(stmt)
|
330
|
+
instance = result.scalar_one_or_none()
|
331
|
+
if instance is None:
|
332
|
+
raise NotFoundException("Object does not exist", 404, "0001")
|
333
|
+
return instance
|
430
334
|
|
431
|
-
|
432
|
-
|
335
|
+
async def is_exists(self, **lookups):
|
336
|
+
stmt = select(self.model).filter_by(**lookups)
|
337
|
+
result = await self.session.execute(stmt)
|
338
|
+
return result.scalar_one_or_none() is not None
|
433
339
|
|
434
|
-
|
435
|
-
- The relationship must be properly defined in the SQLAlchemy model
|
436
|
-
- Uses a non-correlated EXISTS subquery for better performance
|
437
|
-
- Silently continues if the relationship doesn't exist
|
438
|
-
"""
|
439
|
-
# Get the relationship property
|
340
|
+
def has_relation(self, relation_name: str):
|
440
341
|
relationship = getattr(self.model, relation_name)
|
441
|
-
|
442
|
-
# Create subquery using select
|
443
342
|
subquery = (
|
444
343
|
select(1)
|
445
344
|
.select_from(relationship.property.mapper.class_)
|
446
345
|
.where(relationship.property.primaryjoin)
|
447
346
|
.exists()
|
448
347
|
)
|
449
|
-
|
450
|
-
# Add the exists condition to filters
|
451
348
|
self.filters.append(subquery)
|
452
|
-
|
349
|
+
self._filtered = True
|
453
350
|
return self
|
454
351
|
|
455
|
-
def model_to_dict(self, instance: ModelType, exclude: set = None):
|
352
|
+
def model_to_dict(self, instance: ModelType, exclude: set[str] | None = None):
|
456
353
|
exclude = exclude or set()
|
457
|
-
|
458
354
|
return {
|
459
|
-
|
460
|
-
for
|
461
|
-
if
|
355
|
+
col.key: getattr(instance, col.key)
|
356
|
+
for col in inspect(instance).mapper.column_attrs
|
357
|
+
if col.key not in exclude
|
462
358
|
}
|
463
359
|
|
360
|
+
def _ensure_filtered(self):
|
361
|
+
if not self._filtered:
|
362
|
+
raise RuntimeError("You must call `filter()` before this operation.")
|
363
|
+
|
@@ -11,7 +11,7 @@ rb_commons/http/consul.py,sha256=Ioq72VD1jGwoC96set7n2SgxN40olzI-myA2lwKkYi4,186
|
|
11
11
|
rb_commons/http/exceptions.py,sha256=EGRMr1cRgiJ9Q2tkfANbf0c6-zzXf1CD6J3cmCaT_FA,1885
|
12
12
|
rb_commons/orm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
13
|
rb_commons/orm/exceptions.py,sha256=1aMctiEwrPjyehoXVX1l6ML5ZOhmDkmBISzlTD5ey1Y,509
|
14
|
-
rb_commons/orm/managers.py,sha256=
|
14
|
+
rb_commons/orm/managers.py,sha256=Tk33tD4l94IkliHFBR_BRy-jrc-JgVh9S_mXn8v0rpM,13620
|
15
15
|
rb_commons/orm/services.py,sha256=71eRcJ4TxZvzNz-hLXo12X4U7PGK54ZfbLAb27AjZi8,1589
|
16
16
|
rb_commons/permissions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
17
17
|
rb_commons/permissions/role_permissions.py,sha256=HL50s5RHW1nk2U3-YTESg0EDGkJDu1vN4QchcfK-L5g,988
|
@@ -20,7 +20,7 @@ rb_commons/schemes/jwt.py,sha256=F66JJDhholuOPPzlKeoC6f1TL4gXg4oRUrV5yheNpyo,167
|
|
20
20
|
rb_commons/schemes/pagination.py,sha256=8VZW1wZGJIPR9jEBUgppZUoB4uqP8ORudHkMwvEJSxg,1866
|
21
21
|
rb_commons/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
22
22
|
rb_commons/utils/media.py,sha256=KNY_9SdRa3Rp7d3B1tZaXkhmzVa65RcS62BYwZP1bVM,332
|
23
|
-
rb_commons-0.
|
24
|
-
rb_commons-0.
|
25
|
-
rb_commons-0.
|
26
|
-
rb_commons-0.
|
23
|
+
rb_commons-0.5.0.dist-info/METADATA,sha256=bT0B3meXrn9ME0juZhAq0XFg58MaAJxltKGBzpSHEjg,6570
|
24
|
+
rb_commons-0.5.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
25
|
+
rb_commons-0.5.0.dist-info/top_level.txt,sha256=HPx_WAYo3_fbg1WCeGHsz3wPGio1ucbnrlm2lmqlJog,11
|
26
|
+
rb_commons-0.5.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|