rb-commons 0.4.12__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 -324
- {rb_commons-0.4.12.dist-info → rb_commons-0.5.0.dist-info}/METADATA +1 -1
- {rb_commons-0.4.12.dist-info → rb_commons-0.5.0.dist-info}/RECORD +5 -5
- {rb_commons-0.4.12.dist-info → rb_commons-0.5.0.dist-info}/WHEEL +0 -0
- {rb_commons-0.4.12.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,423 +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())
|
190
|
+
return list({obj.id: obj for obj in rows}.values())
|
183
191
|
|
184
|
-
|
185
|
-
self.
|
186
|
-
|
192
|
+
def _reset_state(self):
|
193
|
+
self.filters.clear()
|
194
|
+
self._filtered = False
|
195
|
+
self._limit = None
|
187
196
|
|
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))
|
188
201
|
if self._limit:
|
189
202
|
stmt = stmt.limit(self._limit)
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
async def paginate(self, limit: int = 10, offset: int = 0, load_all_relations: bool = False) -> List[ModelType]:
|
195
|
-
self._ensure_filtered()
|
196
|
-
stmt = select(self.model).filter(and_(*self.filters)).limit(limit).offset(offset)
|
197
|
-
return await self._execute_query_and_unique_data(stmt, load_all_relations)
|
198
|
-
|
199
|
-
def limit(self, value: int) -> 'BaseManager[ModelType]':
|
200
|
-
"""
|
201
|
-
Set a limit on the number of results returned by queries like `all()` or `first()`.
|
202
|
-
"""
|
203
|
-
self._limit = value
|
204
|
-
return self
|
203
|
+
try:
|
204
|
+
return await self._execute_query(stmt, load_all_relations)
|
205
|
+
finally:
|
206
|
+
self._reset_state()
|
205
207
|
|
206
|
-
async def first(self, load_relations: Sequence[str] = None)
|
207
|
-
"""Return the first matching object, or None."""
|
208
|
+
async def first(self, load_relations: Optional[Sequence[str]] = None):
|
208
209
|
self._ensure_filtered()
|
209
|
-
|
210
210
|
stmt = select(self.model).filter(and_(*self.filters))
|
211
|
-
|
212
211
|
if load_relations:
|
213
212
|
for rel in load_relations:
|
214
213
|
stmt = stmt.options(selectinload(getattr(self.model, rel)))
|
215
|
-
|
216
214
|
result = await self.session.execute(stmt)
|
215
|
+
self._reset_state()
|
217
216
|
return result.scalars().first()
|
218
217
|
|
219
|
-
async def last(self, load_relations: Sequence[str] = None)
|
220
|
-
"""Return the last matching object, or None."""
|
221
|
-
|
218
|
+
async def last(self, load_relations: Optional[Sequence[str]] = None):
|
222
219
|
self._ensure_filtered()
|
223
|
-
|
224
|
-
|
225
|
-
|
220
|
+
stmt = (
|
221
|
+
select(self.model)
|
222
|
+
.filter(and_(*self.filters))
|
223
|
+
.order_by(desc(self.model.id))
|
224
|
+
)
|
226
225
|
if load_relations:
|
227
226
|
for rel in load_relations:
|
228
227
|
stmt = stmt.options(selectinload(getattr(self.model, rel)))
|
229
|
-
|
230
228
|
result = await self.session.execute(stmt)
|
229
|
+
self._reset_state()
|
231
230
|
return result.scalars().first()
|
232
231
|
|
233
232
|
async def count(self) -> int:
|
234
|
-
"""Return the count of matching records."""
|
235
|
-
|
236
233
|
self._ensure_filtered()
|
237
234
|
query = select(func.count()).select_from(self.model).filter(and_(*self.filters))
|
238
235
|
result = await self.session.execute(query)
|
236
|
+
self._reset_state()
|
239
237
|
return result.scalar_one()
|
240
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
|
+
|
241
257
|
@with_transaction_error_handling
|
242
|
-
async def create(self, **kwargs)
|
243
|
-
"""
|
244
|
-
Create a new object
|
245
|
-
"""
|
258
|
+
async def create(self, **kwargs):
|
246
259
|
obj = self.model(**kwargs)
|
247
|
-
|
248
260
|
self.session.add(obj)
|
249
261
|
await self.session.flush()
|
250
262
|
return await self._smart_commit(obj)
|
251
263
|
|
252
264
|
@with_transaction_error_handling
|
253
|
-
async def
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
- If `instance` is not provided, delete according to self.filters.
|
258
|
-
|
259
|
-
:arg instance: Model instance to delete.
|
260
|
-
:return: Number of deleted records or None
|
261
|
-
"""
|
262
|
-
|
263
|
-
if instance is not None:
|
264
|
-
await self.session.delete(instance)
|
265
|
-
await self.session.commit()
|
266
|
-
return True
|
267
|
-
|
268
|
-
self._ensure_filtered()
|
269
|
-
|
270
|
-
try:
|
271
|
-
delete_stmt = delete(self.model).where(and_(*self.filters))
|
272
|
-
await self.session.execute(delete_stmt)
|
273
|
-
await self.session.commit()
|
274
|
-
return True
|
275
|
-
except NoResultFound:
|
276
|
-
return False
|
277
|
-
|
278
|
-
@with_transaction_error_handling
|
279
|
-
async def bulk_delete(self) -> int:
|
280
|
-
"""
|
281
|
-
Bulk delete with flexible filtering.
|
282
|
-
|
283
|
-
Automatically commits if not inside a transaction.
|
284
|
-
|
285
|
-
:return: Number of deleted records
|
286
|
-
"""
|
287
|
-
self._ensure_filtered()
|
288
|
-
|
289
|
-
delete_stmt = delete(self.model).where(and_(*self.filters))
|
290
|
-
result = await self.session.execute(delete_stmt)
|
291
|
-
await self._smart_commit()
|
292
|
-
return result.rowcount # ignore
|
265
|
+
async def save(self, instance: ModelType):
|
266
|
+
self.session.add(instance)
|
267
|
+
await self.session.flush()
|
268
|
+
return await self._smart_commit(instance)
|
293
269
|
|
294
270
|
@with_transaction_error_handling
|
295
|
-
async def
|
296
|
-
|
297
|
-
Update object(s) with flexible filtering options
|
298
|
-
|
299
|
-
:param filters: Conditions for selecting records to update
|
300
|
-
:param update_fields: Fields and values to update
|
301
|
-
:return: Number of updated records
|
302
|
-
"""
|
303
|
-
if not update_fields:
|
271
|
+
async def update(self, instance: ModelType, **fields):
|
272
|
+
if not fields:
|
304
273
|
raise InternalException("No fields provided for update")
|
305
|
-
|
306
|
-
|
307
|
-
await self.session.execute(update_stmt)
|
308
|
-
await self.session.commit()
|
309
|
-
updated_instance = await self.get(**filters)
|
310
|
-
return updated_instance
|
311
|
-
|
312
|
-
@with_transaction_error_handling
|
313
|
-
async def update(self, instance: ModelType, **update_fields) -> Optional[ModelType]:
|
314
|
-
"""
|
315
|
-
Update an existing database instance with new fields
|
316
|
-
|
317
|
-
:param instance: The database model instance to update
|
318
|
-
:param update_fields: Keyword arguments of fields to update
|
319
|
-
:return: The updated instance
|
320
|
-
|
321
|
-
:raises InternalException: If integrity violation
|
322
|
-
:raises DatabaseException: For database-related errors
|
323
|
-
"""
|
324
|
-
# Validate update fields
|
325
|
-
if not update_fields:
|
326
|
-
raise InternalException("No fields provided for update")
|
327
|
-
|
328
|
-
# Apply updates directly to the instance
|
329
|
-
for key, value in update_fields.items():
|
330
|
-
setattr(instance, key, value)
|
331
|
-
|
274
|
+
for k, v in fields.items():
|
275
|
+
setattr(instance, k, v)
|
332
276
|
self.session.add(instance)
|
333
277
|
await self._smart_commit()
|
334
|
-
|
335
278
|
return instance
|
336
279
|
|
337
280
|
@with_transaction_error_handling
|
338
|
-
async def
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
Automatically commits if not inside a transaction.
|
346
|
-
|
347
|
-
:raises InternalException: If integrity violation
|
348
|
-
:raises DatabaseException: For database-related errors
|
349
|
-
"""
|
350
|
-
self.session.add(instance)
|
351
|
-
await self.session.flush()
|
352
|
-
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)
|
353
288
|
|
354
289
|
@with_transaction_error_handling
|
355
|
-
async def
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
stmt = select(self.model).filter_by(id=instance.id)
|
368
|
-
|
369
|
-
for rel in load_relations:
|
370
|
-
stmt = stmt.options(selectinload(getattr(self.model, rel)))
|
371
|
-
|
372
|
-
result = await self.session.execute(stmt)
|
373
|
-
loaded_instance = result.scalar_one_or_none()
|
374
|
-
|
375
|
-
if loaded_instance is None:
|
376
|
-
raise NotFoundException(
|
377
|
-
message="Object saved but could not be retrieved with relationships",
|
378
|
-
status=404,
|
379
|
-
code="0001",
|
380
|
-
)
|
381
|
-
|
382
|
-
return loaded_instance
|
383
|
-
|
384
|
-
async def is_exists(self, **kwargs) -> bool:
|
385
|
-
stmt = select(self.model).filter_by(**kwargs)
|
386
|
-
result = await self.session.execute(stmt)
|
387
|
-
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
|
388
301
|
|
389
302
|
@with_transaction_error_handling
|
390
|
-
async def bulk_save(self, instances:
|
391
|
-
"""
|
392
|
-
Bulk save a list of instances into the database.
|
393
|
-
|
394
|
-
If inside a transaction, flushes only.
|
395
|
-
If not in a transaction, commits after flush.
|
396
|
-
|
397
|
-
:param instances: List of instances
|
398
|
-
:raises DatabaseException: If any database error occurs
|
399
|
-
:raises InternalException: If any unexpected error occurs
|
400
|
-
"""
|
303
|
+
async def bulk_save(self, instances: Iterable[ModelType]):
|
401
304
|
if not instances:
|
402
305
|
return
|
403
|
-
|
404
|
-
self.session.add_all(instances)
|
306
|
+
self.session.add_all(list(instances))
|
405
307
|
await self.session.flush()
|
406
|
-
|
407
308
|
if not self.session.in_transaction():
|
408
309
|
await self.session.commit()
|
409
310
|
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
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
|
418
334
|
|
419
|
-
|
420
|
-
|
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
|
421
339
|
|
422
|
-
|
423
|
-
- The relationship must be properly defined in the SQLAlchemy model
|
424
|
-
- Uses a non-correlated EXISTS subquery for better performance
|
425
|
-
- Silently continues if the relationship doesn't exist
|
426
|
-
"""
|
427
|
-
# Get the relationship property
|
340
|
+
def has_relation(self, relation_name: str):
|
428
341
|
relationship = getattr(self.model, relation_name)
|
429
|
-
|
430
|
-
# Create subquery using select
|
431
342
|
subquery = (
|
432
343
|
select(1)
|
433
344
|
.select_from(relationship.property.mapper.class_)
|
434
345
|
.where(relationship.property.primaryjoin)
|
435
346
|
.exists()
|
436
347
|
)
|
437
|
-
|
438
|
-
# Add the exists condition to filters
|
439
348
|
self.filters.append(subquery)
|
440
|
-
|
349
|
+
self._filtered = True
|
441
350
|
return self
|
442
351
|
|
443
|
-
def model_to_dict(self, instance: ModelType, exclude: set = None):
|
352
|
+
def model_to_dict(self, instance: ModelType, exclude: set[str] | None = None):
|
444
353
|
exclude = exclude or set()
|
445
|
-
|
446
354
|
return {
|
447
|
-
|
448
|
-
for
|
449
|
-
if
|
355
|
+
col.key: getattr(instance, col.key)
|
356
|
+
for col in inspect(instance).mapper.column_attrs
|
357
|
+
if col.key not in exclude
|
450
358
|
}
|
451
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
|