rb-commons 0.5.28__py3-none-any.whl → 0.6.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 +89 -133
- {rb_commons-0.5.28.dist-info → rb_commons-0.6.1.dist-info}/METADATA +1 -1
- {rb_commons-0.5.28.dist-info → rb_commons-0.6.1.dist-info}/RECORD +5 -5
- {rb_commons-0.5.28.dist-info → rb_commons-0.6.1.dist-info}/WHEEL +0 -0
- {rb_commons-0.5.28.dist-info → rb_commons-0.6.1.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
|
|
@@ -88,6 +89,9 @@ class BaseManager(Generic[ModelType]):
|
|
88
89
|
self._order_by: List[Any] = []
|
89
90
|
self._joins: set[str] = set()
|
90
91
|
|
92
|
+
mapper = inspect(self.model)
|
93
|
+
self._column_keys = [c.key for c in mapper.mapper.column_attrs]
|
94
|
+
|
91
95
|
async def _smart_commit(self, instance: Optional[ModelType] = None) -> Optional[ModelType]:
|
92
96
|
if not self.session.in_transaction():
|
93
97
|
await self.session.commit()
|
@@ -117,38 +121,39 @@ class BaseManager(Generic[ModelType]):
|
|
117
121
|
return col.is_(None) if value else col.isnot(None)
|
118
122
|
raise ValueError(f"Unsupported operator: {operator}")
|
119
123
|
|
120
|
-
|
124
|
+
@lru_cache(maxsize=None)
|
125
|
+
def _parse_lookup_meta(self, lookup: str):
|
126
|
+
"""
|
127
|
+
One-time parse of "foo__bar__lt" into:
|
128
|
+
- parts = ["foo","bar"]
|
129
|
+
- operator="lt"
|
130
|
+
- relationship_attr, column_attr pointers
|
131
|
+
"""
|
132
|
+
|
121
133
|
parts = lookup.split("__")
|
122
134
|
operator = "eq"
|
123
135
|
if parts[-1] in {"eq", "ne", "gt", "lt", "gte", "lte", "in", "contains", "null"}:
|
124
136
|
operator = parts.pop()
|
125
137
|
|
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_
|
138
|
-
else:
|
139
|
-
attr = candidate
|
140
|
-
|
141
|
-
if relationship_attr:
|
142
|
-
col = attr
|
143
|
-
expr = self._build_comparison(col, operator, value)
|
144
|
-
prop = relationship_attr.property
|
145
|
-
if getattr(prop, "uselist", False):
|
146
|
-
return relationship_attr.any(expr)
|
138
|
+
current = self.model
|
139
|
+
rel = None
|
140
|
+
col = None
|
141
|
+
for p in parts:
|
142
|
+
a = getattr(current, p)
|
143
|
+
if hasattr(a, "property") and isinstance(a.property, RelationshipProperty):
|
144
|
+
rel = a
|
145
|
+
current = a.property.mapper.class_
|
147
146
|
else:
|
148
|
-
|
147
|
+
col = a
|
148
|
+
return parts, operator, rel, col
|
149
149
|
|
150
|
-
|
151
|
-
|
150
|
+
def _parse_lookup(self, lookup: str, value: Any):
|
151
|
+
parts, operator, rel_attr, col_attr = self._parse_lookup_meta(lookup)
|
152
|
+
expr = self._build_comparison(col_attr, operator, value)
|
153
|
+
if rel_attr:
|
154
|
+
prop = rel_attr.property
|
155
|
+
return prop.uselist and rel_attr.any(expr) or rel_attr.has(expr)
|
156
|
+
return expr
|
152
157
|
|
153
158
|
def _q_to_expr(self, q: Union[Q, QJSON]):
|
154
159
|
if isinstance(q, QJSON):
|
@@ -187,27 +192,6 @@ class BaseManager(Generic[ModelType]):
|
|
187
192
|
return json_expr.in_(qjson.value)
|
188
193
|
raise ValueError(f"Unsupported QJSON operator: {qjson.operator}")
|
189
194
|
|
190
|
-
def _loader_from_path(self, path: str) -> Load:
|
191
|
-
"""
|
192
|
-
Turn 'attributes.attribute.attribute_group' into
|
193
|
-
selectinload(Product.attributes)
|
194
|
-
.selectinload(Attribute.attribute)
|
195
|
-
.selectinload(ProductAttributeGroup.attribute_group)
|
196
|
-
"""
|
197
|
-
parts = path.split(".")
|
198
|
-
current_model = self.model
|
199
|
-
loader = None
|
200
|
-
|
201
|
-
for segment in parts:
|
202
|
-
attr = getattr(current_model, segment, None)
|
203
|
-
if attr is None or not hasattr(attr, "property"):
|
204
|
-
raise ValueError(f"Invalid relationship path: {path!r}")
|
205
|
-
|
206
|
-
loader = selectinload(attr) if loader is None else loader.selectinload(attr)
|
207
|
-
current_model = attr.property.mapper.class_ # step down the graph
|
208
|
-
|
209
|
-
return loader
|
210
|
-
|
211
195
|
def order_by(self, *columns: Any):
|
212
196
|
"""Collect ORDER BY clauses.
|
213
197
|
"""
|
@@ -266,28 +250,22 @@ class BaseManager(Generic[ModelType]):
|
|
266
250
|
self._limit = value
|
267
251
|
return self
|
268
252
|
|
269
|
-
def
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
recurse(self.model)
|
287
|
-
return stmt.options(*opts)
|
288
|
-
|
289
|
-
async def _execute_query(self, stmt, load_all_relations: bool):
|
290
|
-
stmt = self._apply_eager_loading(stmt, load_all_relations)
|
253
|
+
def _build_relation_loaders(self, model: Any, relations: Sequence[str]) -> List[Load]:
|
254
|
+
"""
|
255
|
+
Turn ['cat','product__tags'] into
|
256
|
+
[selectinload(model.cat),
|
257
|
+
selectinload(model.product).selectinload('tags')]
|
258
|
+
"""
|
259
|
+
loaders: List[Load] = []
|
260
|
+
for path in relations:
|
261
|
+
parts = path.split("__")
|
262
|
+
loader = selectinload(getattr(model, parts[0]))
|
263
|
+
for sub in parts[1:]:
|
264
|
+
loader = loader.selectinload(sub)
|
265
|
+
loaders.append(loader)
|
266
|
+
return loaders
|
267
|
+
|
268
|
+
async def _execute_query(self, stmt):
|
291
269
|
result = await self.session.execute(stmt)
|
292
270
|
rows = result.scalars().all()
|
293
271
|
return list({obj.id: obj for obj in rows}.values())
|
@@ -298,20 +276,12 @@ class BaseManager(Generic[ModelType]):
|
|
298
276
|
self._limit = None
|
299
277
|
self._joins.clear()
|
300
278
|
|
301
|
-
async def all(self,
|
279
|
+
async def all(self, relations: Optional[List[str]] = None):
|
302
280
|
stmt = select(self.model)
|
303
281
|
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
for part in rel_path.split("__"):
|
309
|
-
join_attr = getattr(rel_model, part)
|
310
|
-
if not hasattr(join_attr, "property"):
|
311
|
-
raise ValueError(f"Invalid join path: {rel_path}")
|
312
|
-
rel_model = join_attr.property.mapper.class_
|
313
|
-
|
314
|
-
stmt = stmt.join(join_attr)
|
282
|
+
if relations:
|
283
|
+
opts = self._build_relation_loaders(self.model, relations)
|
284
|
+
stmt = stmt.options(*opts)
|
315
285
|
|
316
286
|
if self.filters:
|
317
287
|
stmt = stmt.filter(and_(*self.filters))
|
@@ -319,32 +289,38 @@ class BaseManager(Generic[ModelType]):
|
|
319
289
|
stmt = stmt.order_by(*self._order_by)
|
320
290
|
if self._limit:
|
321
291
|
stmt = stmt.limit(self._limit)
|
292
|
+
|
322
293
|
try:
|
323
|
-
return await self._execute_query(stmt
|
294
|
+
return await self._execute_query(stmt)
|
324
295
|
finally:
|
325
296
|
self._reset_state()
|
326
297
|
|
327
|
-
async def first(self,
|
298
|
+
async def first(self, relations: Optional[Sequence[str]] = None):
|
328
299
|
self._ensure_filtered()
|
329
300
|
stmt = select(self.model).filter(and_(*self.filters))
|
301
|
+
|
330
302
|
if self._order_by:
|
331
303
|
stmt = stmt.order_by(*self._order_by)
|
332
|
-
|
333
|
-
|
334
|
-
|
304
|
+
|
305
|
+
if relations:
|
306
|
+
opts = self._build_relation_loaders(self.model, relations)
|
307
|
+
stmt = stmt.options(*opts)
|
308
|
+
|
335
309
|
result = await self.session.execute(stmt)
|
336
310
|
self._reset_state()
|
337
311
|
return result.scalars().first()
|
338
312
|
|
339
313
|
|
340
|
-
async def last(self,
|
314
|
+
async def last(self, relations: Optional[Sequence[str]] = None):
|
341
315
|
self._ensure_filtered()
|
342
316
|
stmt = select(self.model).filter(and_(*self.filters))
|
343
317
|
order = self._order_by or [self.model.id.desc()]
|
344
|
-
stmt = stmt.order_by(*order[::-1]) # reverse for
|
345
|
-
|
346
|
-
|
347
|
-
|
318
|
+
stmt = stmt.order_by(*order[::-1]) # reverse for last
|
319
|
+
|
320
|
+
if relations:
|
321
|
+
opts = self._build_relation_loaders(self.model, relations)
|
322
|
+
stmt = stmt.options(*opts)
|
323
|
+
|
348
324
|
result = await self.session.execute(stmt)
|
349
325
|
self._reset_state()
|
350
326
|
return result.scalars().first()
|
@@ -359,14 +335,14 @@ class BaseManager(Generic[ModelType]):
|
|
359
335
|
result = await self.session.execute(stmt)
|
360
336
|
return int(result.scalar_one())
|
361
337
|
|
362
|
-
async def paginate(self, limit=10, offset=0
|
338
|
+
async def paginate(self, limit: int = 10, offset: int = 0):
|
363
339
|
self._ensure_filtered()
|
364
340
|
stmt = select(self.model).filter(and_(*self.filters))
|
365
341
|
if self._order_by:
|
366
342
|
stmt = stmt.order_by(*self._order_by)
|
367
343
|
stmt = stmt.limit(limit).offset(offset)
|
368
344
|
try:
|
369
|
-
return await self._execute_query(stmt
|
345
|
+
return await self._execute_query(stmt)
|
370
346
|
finally:
|
371
347
|
self._reset_state()
|
372
348
|
|
@@ -384,34 +360,25 @@ class BaseManager(Generic[ModelType]):
|
|
384
360
|
return await self._smart_commit(instance)
|
385
361
|
|
386
362
|
@with_transaction_error_handling
|
387
|
-
async def lazy_save(self, instance: ModelType,
|
363
|
+
async def lazy_save(self, instance: ModelType, relations: list[str] | None = None) -> ModelType:
|
388
364
|
self.session.add(instance)
|
389
|
-
await self.session.
|
390
|
-
await self._smart_commit(instance)
|
365
|
+
await self.session.commit()
|
391
366
|
|
392
|
-
if
|
367
|
+
if relations is None:
|
368
|
+
from sqlalchemy.inspection import inspect
|
393
369
|
mapper = inspect(self.model)
|
394
|
-
|
370
|
+
relations = [r.key for r in mapper.relationships]
|
395
371
|
|
396
|
-
if not
|
372
|
+
if not relations:
|
397
373
|
return instance
|
398
374
|
|
399
375
|
stmt = select(self.model).filter_by(id=instance.id)
|
400
|
-
|
401
|
-
for rel in load_relations:
|
402
|
-
stmt = stmt.options(selectinload(getattr(self.model, rel)))
|
403
|
-
|
376
|
+
stmt = stmt.options(*self._build_relation_loaders(self.model, relations))
|
404
377
|
result = await self.session.execute(stmt)
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
message="Object saved but could not be retrieved with relationships",
|
410
|
-
status=404,
|
411
|
-
code="0001",
|
412
|
-
)
|
413
|
-
|
414
|
-
return loaded_instance
|
378
|
+
loaded = result.scalar_one_or_none()
|
379
|
+
if loaded is None:
|
380
|
+
raise NotFoundException("Could not reload after save", 404, "0001")
|
381
|
+
return loaded
|
415
382
|
|
416
383
|
@with_transaction_error_handling
|
417
384
|
async def update(self, instance: ModelType, **fields):
|
@@ -463,19 +430,12 @@ class BaseManager(Generic[ModelType]):
|
|
463
430
|
self._reset_state()
|
464
431
|
return result.rowcount
|
465
432
|
|
466
|
-
async def get(
|
467
|
-
self,
|
468
|
-
pk: Union[str, int, uuid.UUID],
|
469
|
-
load_relations: Optional[Sequence[str]] = None,
|
470
|
-
):
|
433
|
+
async def get(self, pk: Union[str, int, uuid.UUID], relations: Optional[Sequence[str]] = None) -> Any:
|
471
434
|
stmt = select(self.model).filter_by(id=pk)
|
472
|
-
if
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
else selectinload(getattr(self.model, rel))
|
477
|
-
)
|
478
|
-
stmt = stmt.options(loader)
|
435
|
+
if relations:
|
436
|
+
opts = self._build_relation_loaders(self.model, relations)
|
437
|
+
stmt = stmt.options(*opts)
|
438
|
+
|
479
439
|
result = await self.session.execute(stmt)
|
480
440
|
instance = result.scalar_one_or_none()
|
481
441
|
if instance is None:
|
@@ -527,13 +487,9 @@ class BaseManager(Generic[ModelType]):
|
|
527
487
|
|
528
488
|
return self
|
529
489
|
|
530
|
-
def model_to_dict(self, instance: ModelType, exclude: set[str]
|
490
|
+
def model_to_dict(self, instance: ModelType, exclude: set[str] = None) -> dict:
|
531
491
|
exclude = exclude or set()
|
532
|
-
return {
|
533
|
-
col.key: getattr(instance, col.key)
|
534
|
-
for col in inspect(instance).mapper.column_attrs
|
535
|
-
if col.key not in exclude
|
536
|
-
}
|
492
|
+
return {k: getattr(instance, k) for k in self._column_keys if k not in exclude}
|
537
493
|
|
538
494
|
def _ensure_filtered(self):
|
539
495
|
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=KgvG-GCvjzzcySr-w23Rp0CtUjJwkNx8eNNDobDrW8c,18130
|
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.1.dist-info/METADATA,sha256=9Cd09KreAXl2XMYtjnScVxKU6C4Crmp-B3EBmZOpoRE,6570
|
26
|
+
rb_commons-0.6.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
27
|
+
rb_commons-0.6.1.dist-info/top_level.txt,sha256=HPx_WAYo3_fbg1WCeGHsz3wPGio1ucbnrlm2lmqlJog,11
|
28
|
+
rb_commons-0.6.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|