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.
@@ -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, NoResultFound
6
+ from sqlalchemy.exc import IntegrityError, SQLAlchemyError
7
7
  from sqlalchemy.ext.asyncio import AsyncSession
8
- from sqlalchemy.orm import declarative_base, InstrumentedAttribute, selectinload, RelationshipProperty, Load
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
- def _parse_lookup(self, lookup: str, value: Any):
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
- current_model = self.model
127
- attr = None
128
- relationship_attr = None
129
- for idx, part in enumerate(parts):
130
- candidate = getattr(current_model, part, None)
131
- if candidate is None:
132
- raise ValueError(f"Invalid filter field: {lookup!r}")
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
- attr = candidate
144
+ col = a
145
+ return parts, operator, rel, col
140
146
 
141
- if relationship_attr:
142
- col = attr
143
- expr = self._build_comparison(col, operator, value)
144
- return relationship_attr.any(expr)
145
-
146
- col = getattr(self.model, parts[0], None) if len(parts) == 1 else attr
147
- return self._build_comparison(col, operator, value)
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 _apply_eager_loading(self, stmt, load_all_relations: bool = False):
266
- if not load_all_relations:
267
- return stmt
268
- opts: List[Any] = []
269
- visited: set[Any] = set()
270
-
271
- def recurse(model, loader=None):
272
- mapper = inspect(model)
273
- if mapper in visited:
274
- return
275
- visited.add(mapper)
276
- for rel in mapper.relationships:
277
- attr = getattr(model, rel.key)
278
- this_loader = selectinload(attr) if loader is None else loader.selectinload(attr)
279
- opts.append(this_loader)
280
- recurse(rel.mapper.class_, this_loader)
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, load_all_relations: bool = False):
276
+ async def all(self, relations: Optional[List[str]] = None):
298
277
  stmt = select(self.model)
299
278
 
300
- for rel_path in self._joins:
301
- rel_model = self.model
302
- join_attr = None
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, load_all_relations)
291
+ return await self._execute_query(stmt)
320
292
  finally:
321
293
  self._reset_state()
322
294
 
323
- async def first(self, load_relations: Optional[Sequence[str]] = None):
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
- if load_relations:
329
- for rel in load_relations:
330
- stmt = stmt.options(selectinload(getattr(self.model, rel)))
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, load_relations: Optional[Sequence[str]] = None):
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 LAST
341
- if load_relations:
342
- for rel in load_relations:
343
- stmt = stmt.options(selectinload(getattr(self.model, rel)))
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, load_all_relations=False):
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, load_all_relations)
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, load_relations: Sequence[str] = None) -> Optional[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.flush()
386
- await self._smart_commit(instance)
362
+ await self.session.commit()
387
363
 
388
- if load_relations is None:
364
+ if relations is None:
365
+ from sqlalchemy.inspection import inspect
389
366
  mapper = inspect(self.model)
390
- load_relations = [rel.key for rel in mapper.relationships]
367
+ relations = [r.key for r in mapper.relationships]
391
368
 
392
- if not load_relations:
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
- loaded_instance = result.scalar_one_or_none()
402
-
403
- if loaded_instance is None:
404
- raise NotFoundException(
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 load_relations:
469
- for rel in load_relations:
470
- loader = (
471
- self._loader_from_path(rel) if "." in rel
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] | None = None):
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rb-commons
3
- Version: 0.5.27
3
+ Version: 0.6.0
4
4
  Summary: Commons of project and simplified orm based on sqlalchemy.
5
5
  Home-page: https://github.com/RoboSell-organization/rb-commons
6
6
  Author: Abdulvoris
@@ -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=HpuqxdB8YIxpyYPi5-CSAHmIsorCmM0125ePDbKIoXo,19858
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.5.27.dist-info/METADATA,sha256=VRl4J2G_SYTcWfsT83O1jSlW4-Dlz4x1Tm8fhnXlRqg,6571
26
- rb_commons-0.5.27.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
- rb_commons-0.5.27.dist-info/top_level.txt,sha256=HPx_WAYo3_fbg1WCeGHsz3wPGio1ucbnrlm2lmqlJog,11
28
- rb_commons-0.5.27.dist-info/RECORD,,
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,,