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.
@@ -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,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
- 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
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
- col = getattr(self.model, parts[0], None) if len(parts) == 1 else attr
151
- 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
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 _apply_eager_loading(self, stmt, load_all_relations: bool = False):
270
- if not load_all_relations:
271
- return stmt
272
- opts: List[Any] = []
273
- visited: set[Any] = set()
274
-
275
- def recurse(model, loader=None):
276
- mapper = inspect(model)
277
- if mapper in visited:
278
- return
279
- visited.add(mapper)
280
- for rel in mapper.relationships:
281
- attr = getattr(model, rel.key)
282
- this_loader = selectinload(attr) if loader is None else loader.selectinload(attr)
283
- opts.append(this_loader)
284
- recurse(rel.mapper.class_, this_loader)
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, load_all_relations: bool = False):
276
+ async def all(self, relations: Optional[List[str]] = None):
302
277
  stmt = select(self.model)
303
278
 
304
- for rel_path in self._joins:
305
- rel_model = self.model
306
- join_attr = None
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, load_all_relations)
291
+ return await self._execute_query(stmt)
324
292
  finally:
325
293
  self._reset_state()
326
294
 
327
- async def first(self, load_relations: Optional[Sequence[str]] = None):
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
- if load_relations:
333
- for rel in load_relations:
334
- 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
+
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, load_relations: Optional[Sequence[str]] = None):
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 LAST
345
- if load_relations:
346
- for rel in load_relations:
347
- 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
+
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, load_all_relations=False):
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, load_all_relations)
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, load_relations: Sequence[str] = None) -> Optional[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.flush()
390
- await self._smart_commit(instance)
362
+ await self.session.commit()
391
363
 
392
- if load_relations is None:
364
+ if relations is None:
365
+ from sqlalchemy.inspection import inspect
393
366
  mapper = inspect(self.model)
394
- load_relations = [rel.key for rel in mapper.relationships]
367
+ relations = [r.key for r in mapper.relationships]
395
368
 
396
- if not load_relations:
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
- loaded_instance = result.scalar_one_or_none()
406
-
407
- if loaded_instance is None:
408
- raise NotFoundException(
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 load_relations:
473
- for rel in load_relations:
474
- loader = (
475
- self._loader_from_path(rel) if "." in rel
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] | None = None):
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rb-commons
3
- Version: 0.5.28
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=bY-yz4clB5lnV3xIGp5HyC4WbTL9HT7KBWQz_cwegDo,20029
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.28.dist-info/METADATA,sha256=YzqneH6c0aE667Pvsa2Z3PnlRL5ji0XMfVmy9mtsBIE,6571
26
- rb_commons-0.5.28.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
- rb_commons-0.5.28.dist-info/top_level.txt,sha256=HPx_WAYo3_fbg1WCeGHsz3wPGio1ucbnrlm2lmqlJog,11
28
- rb_commons-0.5.28.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,,