rb-commons 0.5.28__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 -133
- {rb_commons-0.5.28.dist-info → rb_commons-0.6.0.dist-info}/METADATA +1 -1
- {rb_commons-0.5.28.dist-info → rb_commons-0.6.0.dist-info}/RECORD +5 -5
- {rb_commons-0.5.28.dist-info → rb_commons-0.6.0.dist-info}/WHEEL +0 -0
- {rb_commons-0.5.28.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,38 +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
|
-
|
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)
|
147
|
-
else:
|
148
|
-
return relationship_attr.has(expr)
|
144
|
+
col = a
|
145
|
+
return parts, operator, rel, col
|
149
146
|
|
150
|
-
|
151
|
-
|
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
|
152
154
|
|
153
155
|
def _q_to_expr(self, q: Union[Q, QJSON]):
|
154
156
|
if isinstance(q, QJSON):
|
@@ -187,27 +189,6 @@ class BaseManager(Generic[ModelType]):
|
|
187
189
|
return json_expr.in_(qjson.value)
|
188
190
|
raise ValueError(f"Unsupported QJSON operator: {qjson.operator}")
|
189
191
|
|
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
192
|
def order_by(self, *columns: Any):
|
212
193
|
"""Collect ORDER BY clauses.
|
213
194
|
"""
|
@@ -266,28 +247,22 @@ class BaseManager(Generic[ModelType]):
|
|
266
247
|
self._limit = value
|
267
248
|
return self
|
268
249
|
|
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)
|
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):
|
291
266
|
result = await self.session.execute(stmt)
|
292
267
|
rows = result.scalars().all()
|
293
268
|
return list({obj.id: obj for obj in rows}.values())
|
@@ -298,20 +273,12 @@ class BaseManager(Generic[ModelType]):
|
|
298
273
|
self._limit = None
|
299
274
|
self._joins.clear()
|
300
275
|
|
301
|
-
async def all(self,
|
276
|
+
async def all(self, relations: Optional[List[str]] = None):
|
302
277
|
stmt = select(self.model)
|
303
278
|
|
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)
|
279
|
+
if relations:
|
280
|
+
opts = self._build_relation_loaders(self.model, relations)
|
281
|
+
stmt = stmt.options(*opts)
|
315
282
|
|
316
283
|
if self.filters:
|
317
284
|
stmt = stmt.filter(and_(*self.filters))
|
@@ -319,32 +286,38 @@ class BaseManager(Generic[ModelType]):
|
|
319
286
|
stmt = stmt.order_by(*self._order_by)
|
320
287
|
if self._limit:
|
321
288
|
stmt = stmt.limit(self._limit)
|
289
|
+
|
322
290
|
try:
|
323
|
-
return await self._execute_query(stmt
|
291
|
+
return await self._execute_query(stmt)
|
324
292
|
finally:
|
325
293
|
self._reset_state()
|
326
294
|
|
327
|
-
async def first(self,
|
295
|
+
async def first(self, relations: Optional[Sequence[str]] = None):
|
328
296
|
self._ensure_filtered()
|
329
297
|
stmt = select(self.model).filter(and_(*self.filters))
|
298
|
+
|
330
299
|
if self._order_by:
|
331
300
|
stmt = stmt.order_by(*self._order_by)
|
332
|
-
|
333
|
-
|
334
|
-
|
301
|
+
|
302
|
+
if relations:
|
303
|
+
opts = self._build_relation_loaders(self.model, relations)
|
304
|
+
stmt = stmt.options(*opts)
|
305
|
+
|
335
306
|
result = await self.session.execute(stmt)
|
336
307
|
self._reset_state()
|
337
308
|
return result.scalars().first()
|
338
309
|
|
339
310
|
|
340
|
-
async def last(self,
|
311
|
+
async def last(self, relations: Optional[Sequence[str]] = None):
|
341
312
|
self._ensure_filtered()
|
342
313
|
stmt = select(self.model).filter(and_(*self.filters))
|
343
314
|
order = self._order_by or [self.model.id.desc()]
|
344
|
-
stmt = stmt.order_by(*order[::-1]) # reverse for
|
345
|
-
|
346
|
-
|
347
|
-
|
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
|
+
|
348
321
|
result = await self.session.execute(stmt)
|
349
322
|
self._reset_state()
|
350
323
|
return result.scalars().first()
|
@@ -359,14 +332,14 @@ class BaseManager(Generic[ModelType]):
|
|
359
332
|
result = await self.session.execute(stmt)
|
360
333
|
return int(result.scalar_one())
|
361
334
|
|
362
|
-
async def paginate(self, limit=10, offset=0
|
335
|
+
async def paginate(self, limit: int = 10, offset: int = 0):
|
363
336
|
self._ensure_filtered()
|
364
337
|
stmt = select(self.model).filter(and_(*self.filters))
|
365
338
|
if self._order_by:
|
366
339
|
stmt = stmt.order_by(*self._order_by)
|
367
340
|
stmt = stmt.limit(limit).offset(offset)
|
368
341
|
try:
|
369
|
-
return await self._execute_query(stmt
|
342
|
+
return await self._execute_query(stmt)
|
370
343
|
finally:
|
371
344
|
self._reset_state()
|
372
345
|
|
@@ -384,34 +357,25 @@ class BaseManager(Generic[ModelType]):
|
|
384
357
|
return await self._smart_commit(instance)
|
385
358
|
|
386
359
|
@with_transaction_error_handling
|
387
|
-
async def lazy_save(self, instance: ModelType,
|
360
|
+
async def lazy_save(self, instance: ModelType, relations: list[str] | None = None) -> ModelType:
|
388
361
|
self.session.add(instance)
|
389
|
-
await self.session.
|
390
|
-
await self._smart_commit(instance)
|
362
|
+
await self.session.commit()
|
391
363
|
|
392
|
-
if
|
364
|
+
if relations is None:
|
365
|
+
from sqlalchemy.inspection import inspect
|
393
366
|
mapper = inspect(self.model)
|
394
|
-
|
367
|
+
relations = [r.key for r in mapper.relationships]
|
395
368
|
|
396
|
-
if not
|
369
|
+
if not relations:
|
397
370
|
return instance
|
398
371
|
|
399
372
|
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
|
-
|
373
|
+
stmt = stmt.options(*self._build_relation_loaders(self.model, relations))
|
404
374
|
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
|
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
|
415
379
|
|
416
380
|
@with_transaction_error_handling
|
417
381
|
async def update(self, instance: ModelType, **fields):
|
@@ -463,19 +427,12 @@ class BaseManager(Generic[ModelType]):
|
|
463
427
|
self._reset_state()
|
464
428
|
return result.rowcount
|
465
429
|
|
466
|
-
async def get(
|
467
|
-
self,
|
468
|
-
pk: Union[str, int, uuid.UUID],
|
469
|
-
load_relations: Optional[Sequence[str]] = None,
|
470
|
-
):
|
430
|
+
async def get(self, pk: Union[str, int, uuid.UUID], relations: Optional[Sequence[str]] = None) -> Any:
|
471
431
|
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)
|
432
|
+
if relations:
|
433
|
+
opts = self._build_relation_loaders(self.model, relations)
|
434
|
+
stmt = stmt.options(*opts)
|
435
|
+
|
479
436
|
result = await self.session.execute(stmt)
|
480
437
|
instance = result.scalar_one_or_none()
|
481
438
|
if instance is None:
|
@@ -527,13 +484,9 @@ class BaseManager(Generic[ModelType]):
|
|
527
484
|
|
528
485
|
return self
|
529
486
|
|
530
|
-
def model_to_dict(self, instance: ModelType, exclude: set[str]
|
487
|
+
def model_to_dict(self, instance: ModelType, exclude: set[str] = None) -> dict:
|
531
488
|
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
|
-
}
|
489
|
+
return {k: getattr(instance, k) for k in self._column_keys if k not in exclude}
|
537
490
|
|
538
491
|
def _ensure_filtered(self):
|
539
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
|