rb-commons 0.5.27__py3-none-any.whl → 0.6.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 +86 -129
- {rb_commons-0.5.27.dist-info → rb_commons-0.6.0.dist-info}/METADATA +1 -1
- {rb_commons-0.5.27.dist-info → rb_commons-0.6.0.dist-info}/RECORD +5 -5
- {rb_commons-0.5.27.dist-info → rb_commons-0.6.0.dist-info}/WHEEL +0 -0
- {rb_commons-0.5.27.dist-info → rb_commons-0.6.0.dist-info}/top_level.txt +0 -0
rb_commons/orm/managers.py
CHANGED
@@ -3,12 +3,13 @@ from __future__ import annotations
|
|
3
3
|
import uuid
|
4
4
|
from typing import TypeVar, Type, Generic, Optional, List, Dict, Literal, Union, Sequence, Any, Iterable
|
5
5
|
from sqlalchemy import select, delete, update, and_, func, desc, inspect, or_, asc
|
6
|
-
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
6
|
+
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
7
7
|
from sqlalchemy.ext.asyncio import AsyncSession
|
8
|
-
from sqlalchemy.orm import declarative_base,
|
9
|
-
|
8
|
+
from sqlalchemy.orm import declarative_base, selectinload, RelationshipProperty, Load
|
10
9
|
from rb_commons.http.exceptions import NotFoundException
|
11
10
|
from rb_commons.orm.exceptions import DatabaseException, InternalException
|
11
|
+
from functools import lru_cache
|
12
|
+
|
12
13
|
|
13
14
|
ModelType = TypeVar('ModelType', bound=declarative_base())
|
14
15
|
|
@@ -117,34 +118,39 @@ class BaseManager(Generic[ModelType]):
|
|
117
118
|
return col.is_(None) if value else col.isnot(None)
|
118
119
|
raise ValueError(f"Unsupported operator: {operator}")
|
119
120
|
|
120
|
-
|
121
|
+
@lru_cache(maxsize=None)
|
122
|
+
def _parse_lookup_meta(self, lookup: str):
|
123
|
+
"""
|
124
|
+
One-time parse of "foo__bar__lt" into:
|
125
|
+
- parts = ["foo","bar"]
|
126
|
+
- operator="lt"
|
127
|
+
- relationship_attr, column_attr pointers
|
128
|
+
"""
|
129
|
+
|
121
130
|
parts = lookup.split("__")
|
122
131
|
operator = "eq"
|
123
132
|
if parts[-1] in {"eq", "ne", "gt", "lt", "gte", "lte", "in", "contains", "null"}:
|
124
133
|
operator = parts.pop()
|
125
134
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
for
|
130
|
-
|
131
|
-
if
|
132
|
-
|
133
|
-
|
134
|
-
prop = getattr(candidate, "property", None)
|
135
|
-
if prop and isinstance(prop, RelationshipProperty):
|
136
|
-
relationship_attr = candidate
|
137
|
-
current_model = prop.mapper.class_
|
135
|
+
current = self.model
|
136
|
+
rel = None
|
137
|
+
col = None
|
138
|
+
for p in parts:
|
139
|
+
a = getattr(current, p)
|
140
|
+
if hasattr(a, "property") and isinstance(a.property, RelationshipProperty):
|
141
|
+
rel = a
|
142
|
+
current = a.property.mapper.class_
|
138
143
|
else:
|
139
|
-
|
144
|
+
col = a
|
145
|
+
return parts, operator, rel, col
|
140
146
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
return
|
147
|
+
def _parse_lookup(self, lookup: str, value: Any):
|
148
|
+
parts, operator, rel_attr, col_attr = self._parse_lookup_meta(lookup)
|
149
|
+
expr = self._build_comparison(col_attr, operator, value)
|
150
|
+
if rel_attr:
|
151
|
+
prop = rel_attr.property
|
152
|
+
return prop.uselist and rel_attr.any(expr) or rel_attr.has(expr)
|
153
|
+
return expr
|
148
154
|
|
149
155
|
def _q_to_expr(self, q: Union[Q, QJSON]):
|
150
156
|
if isinstance(q, QJSON):
|
@@ -183,27 +189,6 @@ class BaseManager(Generic[ModelType]):
|
|
183
189
|
return json_expr.in_(qjson.value)
|
184
190
|
raise ValueError(f"Unsupported QJSON operator: {qjson.operator}")
|
185
191
|
|
186
|
-
def _loader_from_path(self, path: str) -> Load:
|
187
|
-
"""
|
188
|
-
Turn 'attributes.attribute.attribute_group' into
|
189
|
-
selectinload(Product.attributes)
|
190
|
-
.selectinload(Attribute.attribute)
|
191
|
-
.selectinload(ProductAttributeGroup.attribute_group)
|
192
|
-
"""
|
193
|
-
parts = path.split(".")
|
194
|
-
current_model = self.model
|
195
|
-
loader = None
|
196
|
-
|
197
|
-
for segment in parts:
|
198
|
-
attr = getattr(current_model, segment, None)
|
199
|
-
if attr is None or not hasattr(attr, "property"):
|
200
|
-
raise ValueError(f"Invalid relationship path: {path!r}")
|
201
|
-
|
202
|
-
loader = selectinload(attr) if loader is None else loader.selectinload(attr)
|
203
|
-
current_model = attr.property.mapper.class_ # step down the graph
|
204
|
-
|
205
|
-
return loader
|
206
|
-
|
207
192
|
def order_by(self, *columns: Any):
|
208
193
|
"""Collect ORDER BY clauses.
|
209
194
|
"""
|
@@ -262,28 +247,22 @@ class BaseManager(Generic[ModelType]):
|
|
262
247
|
self._limit = value
|
263
248
|
return self
|
264
249
|
|
265
|
-
def
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
recurse(self.model)
|
283
|
-
return stmt.options(*opts)
|
284
|
-
|
285
|
-
async def _execute_query(self, stmt, load_all_relations: bool):
|
286
|
-
stmt = self._apply_eager_loading(stmt, load_all_relations)
|
250
|
+
def _build_relation_loaders(self, model: Any, relations: Sequence[str]) -> List[Load]:
|
251
|
+
"""
|
252
|
+
Turn ['cat','product__tags'] into
|
253
|
+
[selectinload(model.cat),
|
254
|
+
selectinload(model.product).selectinload('tags')]
|
255
|
+
"""
|
256
|
+
loaders: List[Load] = []
|
257
|
+
for path in relations:
|
258
|
+
parts = path.split("__")
|
259
|
+
loader = selectinload(getattr(model, parts[0]))
|
260
|
+
for sub in parts[1:]:
|
261
|
+
loader = loader.selectinload(sub)
|
262
|
+
loaders.append(loader)
|
263
|
+
return loaders
|
264
|
+
|
265
|
+
async def _execute_query(self, stmt):
|
287
266
|
result = await self.session.execute(stmt)
|
288
267
|
rows = result.scalars().all()
|
289
268
|
return list({obj.id: obj for obj in rows}.values())
|
@@ -294,20 +273,12 @@ class BaseManager(Generic[ModelType]):
|
|
294
273
|
self._limit = None
|
295
274
|
self._joins.clear()
|
296
275
|
|
297
|
-
async def all(self,
|
276
|
+
async def all(self, relations: Optional[List[str]] = None):
|
298
277
|
stmt = select(self.model)
|
299
278
|
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
for part in rel_path.split("__"):
|
305
|
-
join_attr = getattr(rel_model, part)
|
306
|
-
if not hasattr(join_attr, "property"):
|
307
|
-
raise ValueError(f"Invalid join path: {rel_path}")
|
308
|
-
rel_model = join_attr.property.mapper.class_
|
309
|
-
|
310
|
-
stmt = stmt.join(join_attr)
|
279
|
+
if relations:
|
280
|
+
opts = self._build_relation_loaders(self.model, relations)
|
281
|
+
stmt = stmt.options(*opts)
|
311
282
|
|
312
283
|
if self.filters:
|
313
284
|
stmt = stmt.filter(and_(*self.filters))
|
@@ -315,32 +286,38 @@ class BaseManager(Generic[ModelType]):
|
|
315
286
|
stmt = stmt.order_by(*self._order_by)
|
316
287
|
if self._limit:
|
317
288
|
stmt = stmt.limit(self._limit)
|
289
|
+
|
318
290
|
try:
|
319
|
-
return await self._execute_query(stmt
|
291
|
+
return await self._execute_query(stmt)
|
320
292
|
finally:
|
321
293
|
self._reset_state()
|
322
294
|
|
323
|
-
async def first(self,
|
295
|
+
async def first(self, relations: Optional[Sequence[str]] = None):
|
324
296
|
self._ensure_filtered()
|
325
297
|
stmt = select(self.model).filter(and_(*self.filters))
|
298
|
+
|
326
299
|
if self._order_by:
|
327
300
|
stmt = stmt.order_by(*self._order_by)
|
328
|
-
|
329
|
-
|
330
|
-
|
301
|
+
|
302
|
+
if relations:
|
303
|
+
opts = self._build_relation_loaders(self.model, relations)
|
304
|
+
stmt = stmt.options(*opts)
|
305
|
+
|
331
306
|
result = await self.session.execute(stmt)
|
332
307
|
self._reset_state()
|
333
308
|
return result.scalars().first()
|
334
309
|
|
335
310
|
|
336
|
-
async def last(self,
|
311
|
+
async def last(self, relations: Optional[Sequence[str]] = None):
|
337
312
|
self._ensure_filtered()
|
338
313
|
stmt = select(self.model).filter(and_(*self.filters))
|
339
314
|
order = self._order_by or [self.model.id.desc()]
|
340
|
-
stmt = stmt.order_by(*order[::-1]) # reverse for
|
341
|
-
|
342
|
-
|
343
|
-
|
315
|
+
stmt = stmt.order_by(*order[::-1]) # reverse for last
|
316
|
+
|
317
|
+
if relations:
|
318
|
+
opts = self._build_relation_loaders(self.model, relations)
|
319
|
+
stmt = stmt.options(*opts)
|
320
|
+
|
344
321
|
result = await self.session.execute(stmt)
|
345
322
|
self._reset_state()
|
346
323
|
return result.scalars().first()
|
@@ -355,14 +332,14 @@ class BaseManager(Generic[ModelType]):
|
|
355
332
|
result = await self.session.execute(stmt)
|
356
333
|
return int(result.scalar_one())
|
357
334
|
|
358
|
-
async def paginate(self, limit=10, offset=0
|
335
|
+
async def paginate(self, limit: int = 10, offset: int = 0):
|
359
336
|
self._ensure_filtered()
|
360
337
|
stmt = select(self.model).filter(and_(*self.filters))
|
361
338
|
if self._order_by:
|
362
339
|
stmt = stmt.order_by(*self._order_by)
|
363
340
|
stmt = stmt.limit(limit).offset(offset)
|
364
341
|
try:
|
365
|
-
return await self._execute_query(stmt
|
342
|
+
return await self._execute_query(stmt)
|
366
343
|
finally:
|
367
344
|
self._reset_state()
|
368
345
|
|
@@ -380,34 +357,25 @@ class BaseManager(Generic[ModelType]):
|
|
380
357
|
return await self._smart_commit(instance)
|
381
358
|
|
382
359
|
@with_transaction_error_handling
|
383
|
-
async def lazy_save(self, instance: ModelType,
|
360
|
+
async def lazy_save(self, instance: ModelType, relations: list[str] | None = None) -> ModelType:
|
384
361
|
self.session.add(instance)
|
385
|
-
await self.session.
|
386
|
-
await self._smart_commit(instance)
|
362
|
+
await self.session.commit()
|
387
363
|
|
388
|
-
if
|
364
|
+
if relations is None:
|
365
|
+
from sqlalchemy.inspection import inspect
|
389
366
|
mapper = inspect(self.model)
|
390
|
-
|
367
|
+
relations = [r.key for r in mapper.relationships]
|
391
368
|
|
392
|
-
if not
|
369
|
+
if not relations:
|
393
370
|
return instance
|
394
371
|
|
395
372
|
stmt = select(self.model).filter_by(id=instance.id)
|
396
|
-
|
397
|
-
for rel in load_relations:
|
398
|
-
stmt = stmt.options(selectinload(getattr(self.model, rel)))
|
399
|
-
|
373
|
+
stmt = stmt.options(*self._build_relation_loaders(self.model, relations))
|
400
374
|
result = await self.session.execute(stmt)
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
message="Object saved but could not be retrieved with relationships",
|
406
|
-
status=404,
|
407
|
-
code="0001",
|
408
|
-
)
|
409
|
-
|
410
|
-
return loaded_instance
|
375
|
+
loaded = result.scalar_one_or_none()
|
376
|
+
if loaded is None:
|
377
|
+
raise NotFoundException("Could not reload after save", 404, "0001")
|
378
|
+
return loaded
|
411
379
|
|
412
380
|
@with_transaction_error_handling
|
413
381
|
async def update(self, instance: ModelType, **fields):
|
@@ -459,19 +427,12 @@ class BaseManager(Generic[ModelType]):
|
|
459
427
|
self._reset_state()
|
460
428
|
return result.rowcount
|
461
429
|
|
462
|
-
async def get(
|
463
|
-
self,
|
464
|
-
pk: Union[str, int, uuid.UUID],
|
465
|
-
load_relations: Optional[Sequence[str]] = None,
|
466
|
-
):
|
430
|
+
async def get(self, pk: Union[str, int, uuid.UUID], relations: Optional[Sequence[str]] = None) -> Any:
|
467
431
|
stmt = select(self.model).filter_by(id=pk)
|
468
|
-
if
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
else selectinload(getattr(self.model, rel))
|
473
|
-
)
|
474
|
-
stmt = stmt.options(loader)
|
432
|
+
if relations:
|
433
|
+
opts = self._build_relation_loaders(self.model, relations)
|
434
|
+
stmt = stmt.options(*opts)
|
435
|
+
|
475
436
|
result = await self.session.execute(stmt)
|
476
437
|
instance = result.scalar_one_or_none()
|
477
438
|
if instance is None:
|
@@ -523,13 +484,9 @@ class BaseManager(Generic[ModelType]):
|
|
523
484
|
|
524
485
|
return self
|
525
486
|
|
526
|
-
def model_to_dict(self, instance: ModelType, exclude: set[str]
|
487
|
+
def model_to_dict(self, instance: ModelType, exclude: set[str] = None) -> dict:
|
527
488
|
exclude = exclude or set()
|
528
|
-
return {
|
529
|
-
col.key: getattr(instance, col.key)
|
530
|
-
for col in inspect(instance).mapper.column_attrs
|
531
|
-
if col.key not in exclude
|
532
|
-
}
|
489
|
+
return {k: getattr(instance, k) for k in self._column_keys if k not in exclude}
|
533
490
|
|
534
491
|
def _ensure_filtered(self):
|
535
492
|
if not self._filtered:
|
@@ -13,7 +13,7 @@ rb_commons/http/consul.py,sha256=Ioq72VD1jGwoC96set7n2SgxN40olzI-myA2lwKkYi4,186
|
|
13
13
|
rb_commons/http/exceptions.py,sha256=EGRMr1cRgiJ9Q2tkfANbf0c6-zzXf1CD6J3cmCaT_FA,1885
|
14
14
|
rb_commons/orm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
15
|
rb_commons/orm/exceptions.py,sha256=1aMctiEwrPjyehoXVX1l6ML5ZOhmDkmBISzlTD5ey1Y,509
|
16
|
-
rb_commons/orm/managers.py,sha256=
|
16
|
+
rb_commons/orm/managers.py,sha256=3Nd3NcyGGwcmhid7IW7BDQMHLms9oTh-QFw0VXXioes,18017
|
17
17
|
rb_commons/orm/services.py,sha256=71eRcJ4TxZvzNz-hLXo12X4U7PGK54ZfbLAb27AjZi8,1589
|
18
18
|
rb_commons/permissions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
19
19
|
rb_commons/permissions/role_permissions.py,sha256=4dV89z6ggzLqCCiFYlMp7kQVJRESu6MHpkT5ZNjLo6A,1096
|
@@ -22,7 +22,7 @@ rb_commons/schemes/jwt.py,sha256=ZKLJ5D3fcEmEKySjzbxEgUcza4K-oPoHr14_Z0r9Yic,249
|
|
22
22
|
rb_commons/schemes/pagination.py,sha256=8VZW1wZGJIPR9jEBUgppZUoB4uqP8ORudHkMwvEJSxg,1866
|
23
23
|
rb_commons/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
24
24
|
rb_commons/utils/media.py,sha256=J2Zi0J28DhcVQVzt-myNNVuzj9Msaetul53VjZtdDdc,820
|
25
|
-
rb_commons-0.
|
26
|
-
rb_commons-0.
|
27
|
-
rb_commons-0.
|
28
|
-
rb_commons-0.
|
25
|
+
rb_commons-0.6.0.dist-info/METADATA,sha256=G43YUKJDIEZ4LtcE-y2asZ6slct6akHsXZBxynASlHU,6570
|
26
|
+
rb_commons-0.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
27
|
+
rb_commons-0.6.0.dist-info/top_level.txt,sha256=HPx_WAYo3_fbg1WCeGHsz3wPGio1ucbnrlm2lmqlJog,11
|
28
|
+
rb_commons-0.6.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|