rb-commons 0.4.13__py3-none-any.whl → 0.5.1__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 +240 -310
- {rb_commons-0.4.13.dist-info → rb_commons-0.5.1.dist-info}/METADATA +1 -1
- {rb_commons-0.4.13.dist-info → rb_commons-0.5.1.dist-info}/RECORD +5 -5
- {rb_commons-0.4.13.dist-info → rb_commons-0.5.1.dist-info}/WHEEL +0 -0
- {rb_commons-0.4.13.dist-info → rb_commons-0.5.1.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,336 +71,198 @@ 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())
|
190
|
+
return list({obj.id: obj for obj in rows}.values())
|
183
191
|
|
184
|
-
|
185
|
-
|
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)
|
193
|
-
|
194
|
-
self._clear_query_state()
|
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
|
-
Delete object(s) with flexible filtering options
|
268
|
-
- If `instance` is provided, delete that single instance.
|
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
|
323
|
-
|
324
|
-
@with_transaction_error_handling
|
325
|
-
async def update(self, instance: ModelType, **update_fields) -> Optional[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:
|
338
|
-
raise InternalException("No fields provided for update")
|
339
|
-
|
340
|
-
# Apply updates directly to the instance
|
341
|
-
for key, value in update_fields.items():
|
342
|
-
setattr(instance, key, value)
|
343
|
-
|
344
|
-
self.session.add(instance)
|
345
|
-
await self._smart_commit()
|
346
|
-
|
347
|
-
return instance
|
348
|
-
|
349
|
-
@with_transaction_error_handling
|
350
|
-
async def save(self, instance: ModelType) -> Optional[ModelType]:
|
351
|
-
"""
|
352
|
-
Save instance
|
353
|
-
|
354
|
-
:param instance: The database model instance to save
|
355
|
-
:return: The saved instance
|
356
|
-
|
357
|
-
Automatically commits if not inside a transaction.
|
358
|
-
|
359
|
-
:raises InternalException: If integrity violation
|
360
|
-
:raises DatabaseException: For database-related errors
|
361
|
-
"""
|
265
|
+
async def save(self, instance: ModelType):
|
362
266
|
self.session.add(instance)
|
363
267
|
await self.session.flush()
|
364
268
|
return await self._smart_commit(instance)
|
@@ -393,71 +297,97 @@ class BaseManager(Generic[ModelType]):
|
|
393
297
|
|
394
298
|
return loaded_instance
|
395
299
|
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
300
|
+
@with_transaction_error_handling
|
301
|
+
async def update(self, instance: ModelType, **fields):
|
302
|
+
if not fields:
|
303
|
+
raise InternalException("No fields provided for update")
|
304
|
+
for k, v in fields.items():
|
305
|
+
setattr(instance, k, v)
|
306
|
+
self.session.add(instance)
|
307
|
+
await self._smart_commit()
|
308
|
+
return instance
|
400
309
|
|
401
310
|
@with_transaction_error_handling
|
402
|
-
async def
|
403
|
-
|
404
|
-
|
311
|
+
async def update_by_filters(self, filters: Dict[str, Any], **fields):
|
312
|
+
if not fields:
|
313
|
+
raise InternalException("No fields provided for update")
|
314
|
+
stmt = update(self.model).filter_by(**filters).values(**fields)
|
315
|
+
await self.session.execute(stmt)
|
316
|
+
await self.session.commit()
|
317
|
+
return await self.get(**filters)
|
405
318
|
|
406
|
-
|
407
|
-
|
319
|
+
@with_transaction_error_handling
|
320
|
+
async def delete(self, instance: Optional[ModelType] = None):
|
321
|
+
if instance is not None:
|
322
|
+
await self.session.delete(instance)
|
323
|
+
await self.session.commit()
|
324
|
+
return True
|
325
|
+
self._ensure_filtered()
|
326
|
+
stmt = delete(self.model).where(and_(*self.filters))
|
327
|
+
await self.session.execute(stmt)
|
328
|
+
await self.session.commit()
|
329
|
+
self._reset_state()
|
330
|
+
return True
|
408
331
|
|
409
|
-
|
410
|
-
|
411
|
-
:raises InternalException: If any unexpected error occurs
|
412
|
-
"""
|
332
|
+
@with_transaction_error_handling
|
333
|
+
async def bulk_save(self, instances: Iterable[ModelType]):
|
413
334
|
if not instances:
|
414
335
|
return
|
415
|
-
|
416
|
-
self.session.add_all(instances)
|
336
|
+
self.session.add_all(list(instances))
|
417
337
|
await self.session.flush()
|
418
|
-
|
419
338
|
if not self.session.in_transaction():
|
420
339
|
await self.session.commit()
|
421
340
|
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
341
|
+
@with_transaction_error_handling
|
342
|
+
async def bulk_delete(self):
|
343
|
+
self._ensure_filtered()
|
344
|
+
stmt = delete(self.model).where(and_(*self.filters))
|
345
|
+
result = await self.session.execute(stmt)
|
346
|
+
await self._smart_commit()
|
347
|
+
self._reset_state()
|
348
|
+
return result.rowcount
|
349
|
+
|
350
|
+
async def get(
|
351
|
+
self,
|
352
|
+
pk: Union[str, int, uuid.UUID],
|
353
|
+
load_relations: Optional[Sequence[str]] = None,
|
354
|
+
):
|
355
|
+
stmt = select(self.model).filter_by(id=pk)
|
356
|
+
if load_relations:
|
357
|
+
for rel in load_relations:
|
358
|
+
stmt = stmt.options(selectinload(getattr(self.model, rel)))
|
359
|
+
result = await self.session.execute(stmt)
|
360
|
+
instance = result.scalar_one_or_none()
|
361
|
+
if instance is None:
|
362
|
+
raise NotFoundException("Object does not exist", 404, "0001")
|
363
|
+
return instance
|
430
364
|
|
431
|
-
|
432
|
-
|
365
|
+
async def is_exists(self, **lookups):
|
366
|
+
stmt = select(self.model).filter_by(**lookups)
|
367
|
+
result = await self.session.execute(stmt)
|
368
|
+
return result.scalar_one_or_none() is not None
|
433
369
|
|
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
|
370
|
+
def has_relation(self, relation_name: str):
|
440
371
|
relationship = getattr(self.model, relation_name)
|
441
|
-
|
442
|
-
# Create subquery using select
|
443
372
|
subquery = (
|
444
373
|
select(1)
|
445
374
|
.select_from(relationship.property.mapper.class_)
|
446
375
|
.where(relationship.property.primaryjoin)
|
447
376
|
.exists()
|
448
377
|
)
|
449
|
-
|
450
|
-
# Add the exists condition to filters
|
451
378
|
self.filters.append(subquery)
|
452
|
-
|
379
|
+
self._filtered = True
|
453
380
|
return self
|
454
381
|
|
455
|
-
def model_to_dict(self, instance: ModelType, exclude: set = None):
|
382
|
+
def model_to_dict(self, instance: ModelType, exclude: set[str] | None = None):
|
456
383
|
exclude = exclude or set()
|
457
|
-
|
458
384
|
return {
|
459
|
-
|
460
|
-
for
|
461
|
-
if
|
385
|
+
col.key: getattr(instance, col.key)
|
386
|
+
for col in inspect(instance).mapper.column_attrs
|
387
|
+
if col.key not in exclude
|
462
388
|
}
|
463
389
|
|
390
|
+
def _ensure_filtered(self):
|
391
|
+
if not self._filtered:
|
392
|
+
raise RuntimeError("You must call `filter()` before this operation.")
|
393
|
+
|
@@ -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=w0vTrGfugWDC1ccGU2b19DfGQ6_6X3CWMbBzkjpQiFs,14659
|
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.1.dist-info/METADATA,sha256=VDh2GcvBL_Uhvka30qhbR_MCnAcxjP-swb4OPhXyz6Q,6570
|
24
|
+
rb_commons-0.5.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
25
|
+
rb_commons-0.5.1.dist-info/top_level.txt,sha256=HPx_WAYo3_fbg1WCeGHsz3wPGio1ucbnrlm2lmqlJog,11
|
26
|
+
rb_commons-0.5.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|