rb-commons 0.7.3__py3-none-any.whl → 0.7.5__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.
@@ -2,68 +2,18 @@ from __future__ import annotations
2
2
 
3
3
  import re
4
4
  import uuid
5
- from typing import TypeVar, Type, Generic, Optional, List, Dict, Literal, Union, Sequence, Any, Iterable
5
+ from typing import TypeVar, Type, Generic, Optional, List, Dict, Literal, Union, Sequence, Any, Iterable, Callable
6
6
  from sqlalchemy import select, delete, update, and_, func, desc, inspect, or_, asc, true
7
7
  from sqlalchemy.exc import IntegrityError, SQLAlchemyError
8
8
  from sqlalchemy.ext.asyncio import AsyncSession
9
9
  from sqlalchemy.orm import declarative_base, selectinload, RelationshipProperty, Load
10
10
  from rb_commons.http.exceptions import NotFoundException
11
11
  from rb_commons.orm.exceptions import DatabaseException, InternalException
12
- from functools import lru_cache
13
-
12
+ from functools import lru_cache, wraps
13
+ from querysets import Q, QJSON
14
14
 
15
15
  ModelType = TypeVar('ModelType', bound=declarative_base())
16
16
 
17
- class QJSON:
18
- def __init__(self, field: str, key: str, operator: str, value: Any):
19
- self.field = field
20
- self.key = key
21
- self.operator = operator
22
- self.value = value
23
-
24
- def __repr__(self):
25
- return f"QJSON(field={self.field}, key={self.key}, op={self.operator}, value={self.value})"
26
-
27
- class Q:
28
- """Boolean logic container that can be combined with `&`, `|`, and `~`."""
29
-
30
- def __init__(self, **lookups: Any) -> None:
31
- self.lookups: Dict[str, Any] = lookups
32
- self.children: List[Q] = []
33
- self._operator: str = "AND"
34
- self.negated: bool = False
35
-
36
- def _combine(self, other: "Q", operator: str) -> "Q":
37
- combined = Q()
38
- combined.children = [self, other]
39
- combined._operator = operator
40
- return combined
41
-
42
- def __or__(self, other: "Q") -> "Q":
43
- return self._combine(other, "OR")
44
-
45
- def __and__(self, other: "Q") -> "Q":
46
- return self._combine(other, "AND")
47
-
48
- def __invert__(self) -> "Q":
49
- clone = Q()
50
- clone.lookups = self.lookups.copy()
51
- clone.children = list(self.children)
52
- clone._operator = self._operator
53
- clone.negated = not self.negated
54
- return clone
55
-
56
- def __repr__(self) -> str:
57
- if self.lookups:
58
- base = f"Q({self.lookups})"
59
- else:
60
- base = "Q()"
61
- if self.children:
62
- base += f" {self._operator} {self.children}"
63
- if self.negated:
64
- base = f"NOT({base})"
65
- return base
66
-
67
17
  def with_transaction_error_handling(func):
68
18
  async def wrapper(self, *args, **kwargs):
69
19
  try:
@@ -79,6 +29,20 @@ def with_transaction_error_handling(func):
79
29
  raise InternalException(f"Unexpected error: {str(e)}") from e
80
30
  return wrapper
81
31
 
32
+ F = TypeVar("F", bound=Callable[..., Any])
33
+
34
+ def query_mutator(func: F) -> F:
35
+ """
36
+ Make a query‑builder method clone‑on‑write without touching its body.
37
+ """
38
+ @wraps(func)
39
+ def wrapper(self: "BaseManager[Any]", *args, **kwargs):
40
+ clone = self._clone()
41
+ result = func(clone, *args, **kwargs)
42
+ return result if result is not None else clone
43
+ return wrapper
44
+
45
+
82
46
  class BaseManager(Generic[ModelType]):
83
47
  model: Type[ModelType]
84
48
 
@@ -93,6 +57,18 @@ class BaseManager(Generic[ModelType]):
93
57
  mapper = inspect(self.model)
94
58
  self._column_keys = [c.key for c in mapper.mapper.column_attrs]
95
59
 
60
+ def _clone(self) -> "BaseManager[ModelType]":
61
+ """
62
+ Shallow‑copy all mutable query state into a new manager instance.
63
+ """
64
+ clone = self.__class__(self.session)
65
+ clone.filters = list(self.filters)
66
+ clone._order_by = list(self._order_by)
67
+ clone._limit = self._limit
68
+ clone._joins = set(self._joins)
69
+ clone._filtered = self._filtered
70
+ return clone
71
+
96
72
  async def _smart_commit(self, instance: Optional[ModelType] = None) -> Optional[ModelType]:
97
73
  if not self.session.in_transaction():
98
74
  await self.session.commit()
@@ -200,6 +176,66 @@ class BaseManager(Generic[ModelType]):
200
176
  return json_expr.in_(qjson.value)
201
177
  raise ValueError(f"Unsupported QJSON operator: {qjson.operator}")
202
178
 
179
+ def _build_relation_loaders(
180
+ self,
181
+ model: Any,
182
+ relations: Sequence[str] | None = None
183
+ ) -> List[Load]:
184
+ """
185
+ Given e.g. ["media", "properties.property", "properties__property"],
186
+ returns [
187
+ selectinload(Product.media),
188
+ selectinload(Product.properties).selectinload(Property.property)
189
+ ].
190
+
191
+ If `relations` is None or empty, recurse *all* relationships once (cycle-safe).
192
+ """
193
+ loaders: List[Load] = []
194
+
195
+ if relations:
196
+ for path in relations:
197
+ parts = re.split(r"\.|\_\_", path)
198
+ current_model = model
199
+ loader: Load | None = None
200
+
201
+ for part in parts:
202
+ attr = getattr(current_model, part, None)
203
+ if attr is None or not hasattr(attr, "property"):
204
+ raise ValueError(f"Invalid relationship path: {path!r}")
205
+ loader = selectinload(attr) if loader is None else loader.selectinload(attr)
206
+ current_model = attr.property.mapper.class_
207
+
208
+ loaders.append(loader)
209
+
210
+ return loaders
211
+
212
+ visited = set()
213
+
214
+ def recurse(curr_model: Any, curr_loader: Load | None = None):
215
+ mapper = inspect(curr_model)
216
+ if mapper in visited:
217
+ return
218
+ visited.add(mapper)
219
+
220
+ for rel in mapper.relationships:
221
+ attr = getattr(curr_model, rel.key)
222
+ loader = (
223
+ selectinload(attr)
224
+ if curr_loader is None
225
+ else curr_loader.selectinload(attr)
226
+ )
227
+ loaders.append(loader)
228
+ recurse(rel.mapper.class_, loader)
229
+
230
+ recurse(model)
231
+ return loaders
232
+
233
+ async def _execute_query(self, stmt):
234
+ result = await self.session.execute(stmt)
235
+ rows = result.scalars().all()
236
+ return list({obj.id: obj for obj in rows}.values())
237
+
238
+ @query_mutator
203
239
  def order_by(self, *columns: Any):
204
240
  """Collect ORDER BY clauses.
205
241
  """
@@ -216,6 +252,7 @@ class BaseManager(Generic[ModelType]):
216
252
 
217
253
  return self
218
254
 
255
+ @query_mutator
219
256
  def filter(self, *expressions: Any, **lookups: Any) -> "BaseManager":
220
257
  self._filtered = True
221
258
 
@@ -236,6 +273,7 @@ class BaseManager(Generic[ModelType]):
236
273
 
237
274
  return self
238
275
 
276
+ @query_mutator
239
277
  def or_filter(self, *expressions: Any, **lookups: Any) -> "BaseManager[ModelType]":
240
278
  """Add one OR group (shortcut for `filter(Q() | Q())`)."""
241
279
 
@@ -254,6 +292,7 @@ class BaseManager(Generic[ModelType]):
254
292
  self.filters.append(or_(*or_clauses))
255
293
  return self
256
294
 
295
+ @query_mutator
257
296
  def exclude(self, *expressions: Any, **lookups: Any) -> "BaseManager[ModelType]":
258
297
  """
259
298
  Exclude records that match the given conditions.
@@ -297,75 +336,11 @@ class BaseManager(Generic[ModelType]):
297
336
 
298
337
  return self
299
338
 
339
+ @query_mutator
300
340
  def limit(self, value: int) -> "BaseManager[ModelType]":
301
341
  self._limit = value
302
342
  return self
303
343
 
304
- def _build_relation_loaders(
305
- self,
306
- model: Any,
307
- relations: Sequence[str] | None = None
308
- ) -> List[Load]:
309
- """
310
- Given e.g. ["media", "properties.property", "properties__property"],
311
- returns [
312
- selectinload(Product.media),
313
- selectinload(Product.properties).selectinload(Property.property)
314
- ].
315
-
316
- If `relations` is None or empty, recurse *all* relationships once (cycle-safe).
317
- """
318
- loaders: List[Load] = []
319
-
320
- if relations:
321
- for path in relations:
322
- parts = re.split(r"\.|\_\_", path)
323
- current_model = model
324
- loader: Load | None = None
325
-
326
- for part in parts:
327
- attr = getattr(current_model, part, None)
328
- if attr is None or not hasattr(attr, "property"):
329
- raise ValueError(f"Invalid relationship path: {path!r}")
330
- loader = selectinload(attr) if loader is None else loader.selectinload(attr)
331
- current_model = attr.property.mapper.class_
332
-
333
- loaders.append(loader)
334
-
335
- return loaders
336
-
337
- visited = set()
338
-
339
- def recurse(curr_model: Any, curr_loader: Load | None = None):
340
- mapper = inspect(curr_model)
341
- if mapper in visited:
342
- return
343
- visited.add(mapper)
344
-
345
- for rel in mapper.relationships:
346
- attr = getattr(curr_model, rel.key)
347
- loader = (
348
- selectinload(attr)
349
- if curr_loader is None
350
- else curr_loader.selectinload(attr)
351
- )
352
- loaders.append(loader)
353
- recurse(rel.mapper.class_, loader)
354
-
355
- recurse(model)
356
- return loaders
357
-
358
- async def _execute_query(self, stmt):
359
- result = await self.session.execute(stmt)
360
- rows = result.scalars().all()
361
- return list({obj.id: obj for obj in rows}.values())
362
-
363
- def _reset_state(self):
364
- self.filters.clear()
365
- self._filtered = False
366
- self._limit = None
367
- self._joins.clear()
368
-
369
344
  async def all(self, relations: Optional[List[str]] = None):
370
345
  stmt = select(self.model)
371
346
 
@@ -380,10 +355,7 @@ class BaseManager(Generic[ModelType]):
380
355
  if self._limit:
381
356
  stmt = stmt.limit(self._limit)
382
357
 
383
- try:
384
- return await self._execute_query(stmt)
385
- finally:
386
- self._reset_state()
358
+ return await self._execute_query(stmt)
387
359
 
388
360
  async def first(self, relations: Optional[Sequence[str]] = None):
389
361
  self._ensure_filtered()
@@ -397,22 +369,19 @@ class BaseManager(Generic[ModelType]):
397
369
  stmt = stmt.options(*opts)
398
370
 
399
371
  result = await self.session.execute(stmt)
400
- self._reset_state()
401
372
  return result.scalars().first()
402
373
 
403
-
404
374
  async def last(self, relations: Optional[Sequence[str]] = None):
405
375
  self._ensure_filtered()
406
376
  stmt = select(self.model).filter(and_(*self.filters))
407
377
  order = self._order_by or [self.model.id.desc()]
408
- stmt = stmt.order_by(*order[::-1]) # reverse for last
378
+ stmt = stmt.order_by(*order[::-1])
409
379
 
410
380
  if relations:
411
381
  opts = self._build_relation_loaders(self.model, relations)
412
382
  stmt = stmt.options(*opts)
413
383
 
414
384
  result = await self.session.execute(stmt)
415
- self._reset_state()
416
385
  return result.scalars().first()
417
386
 
418
387
  async def count(self) -> int | None:
@@ -436,10 +405,7 @@ class BaseManager(Generic[ModelType]):
436
405
  if self._order_by:
437
406
  stmt = stmt.order_by(*self._order_by)
438
407
  stmt = stmt.limit(limit).offset(offset)
439
- try:
440
- return await self._execute_query(stmt)
441
- finally:
442
- self._reset_state()
408
+ return await self._execute_query(stmt)
443
409
 
444
410
  @with_transaction_error_handling
445
411
  async def create(self, **kwargs):
@@ -504,7 +470,6 @@ class BaseManager(Generic[ModelType]):
504
470
  stmt = delete(self.model).where(and_(*self.filters))
505
471
  await self.session.execute(stmt)
506
472
  await self.session.commit()
507
- self._reset_state()
508
473
  return True
509
474
 
510
475
  @with_transaction_error_handling
@@ -522,7 +487,6 @@ class BaseManager(Generic[ModelType]):
522
487
  stmt = delete(self.model).where(and_(*self.filters))
523
488
  result = await self.session.execute(stmt)
524
489
  await self._smart_commit()
525
- self._reset_state()
526
490
  return result.rowcount
527
491
 
528
492
  async def get(self, pk: Union[str, int, uuid.UUID], relations: Optional[Sequence[str]] = None) -> Any:
@@ -548,6 +512,7 @@ class BaseManager(Generic[ModelType]):
548
512
  result = await self.session.execute(stmt)
549
513
  return result.scalars().first() is not None
550
514
 
515
+ @query_mutator
551
516
  def has_relation(self, relation_name: str):
552
517
  relationship = getattr(self.model, relation_name)
553
518
  subquery = (
@@ -560,6 +525,7 @@ class BaseManager(Generic[ModelType]):
560
525
  self._filtered = True
561
526
  return self
562
527
 
528
+ @query_mutator
563
529
  def sort_by(self, tokens: Sequence[str]) -> "BaseManager[ModelType]":
564
530
  """
565
531
  Dynamically apply ORDER BY clauses based on a list of "field" or "-field" tokens.
@@ -0,0 +1,56 @@
1
+ from typing import Any
2
+
3
+ from functools import wraps
4
+ from typing import Callable, TypeVar, Any
5
+
6
+ class QJSON:
7
+ def __init__(self, field: str, key: str, operator: str, value: Any):
8
+ self.field = field
9
+ self.key = key
10
+ self.operator = operator
11
+ self.value = value
12
+
13
+ def __repr__(self):
14
+ return f"QJSON(field={self.field}, key={self.key}, op={self.operator}, value={self.value})"
15
+
16
+ class Q:
17
+ """Boolean logic container that can be combined with `&`, `|`, and `~`."""
18
+
19
+ def __init__(self, **lookups: Any) -> None:
20
+ self.lookups: Dict[str, Any] = lookups
21
+ self.children: List[Q] = []
22
+ self._operator: str = "AND"
23
+ self.negated: bool = False
24
+
25
+ def _combine(self, other: "Q", operator: str) -> "Q":
26
+ combined = Q()
27
+ combined.children = [self, other]
28
+ combined._operator = operator
29
+ return combined
30
+
31
+ def __or__(self, other: "Q") -> "Q":
32
+ return self._combine(other, "OR")
33
+
34
+ def __and__(self, other: "Q") -> "Q":
35
+ return self._combine(other, "AND")
36
+
37
+ def __invert__(self) -> "Q":
38
+ clone = Q()
39
+ clone.lookups = self.lookups.copy()
40
+ clone.children = list(self.children)
41
+ clone._operator = self._operator
42
+ clone.negated = not self.negated
43
+ return clone
44
+
45
+ def __repr__(self) -> str:
46
+ if self.lookups:
47
+ base = f"Q({self.lookups})"
48
+ else:
49
+ base = "Q()"
50
+ if self.children:
51
+ base += f" {self._operator} {self.children}"
52
+ if self.negated:
53
+ base = f"NOT({base})"
54
+ return base
55
+
56
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rb-commons
3
- Version: 0.7.3
3
+ Version: 0.7.5
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,8 @@ 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=041roR5ViDlY6e4kOQEGeOuRTirS3pakzatMn1Py-GU,21256
16
+ rb_commons/orm/managers.py,sha256=Wa_2_l5I7XOAgW8tLeW-poioctpVNm7jJP2t1hbuzIk,20304
17
+ rb_commons/orm/querysets.py,sha256=Q4iY_TKZItpNnrbCAvlSwyCxJikhaX5qVF3Y2rVgAYQ,1666
17
18
  rb_commons/orm/services.py,sha256=71eRcJ4TxZvzNz-hLXo12X4U7PGK54ZfbLAb27AjZi8,1589
18
19
  rb_commons/permissions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
20
  rb_commons/permissions/role_permissions.py,sha256=4dV89z6ggzLqCCiFYlMp7kQVJRESu6MHpkT5ZNjLo6A,1096
@@ -22,7 +23,7 @@ rb_commons/schemes/jwt.py,sha256=ZKLJ5D3fcEmEKySjzbxEgUcza4K-oPoHr14_Z0r9Yic,249
22
23
  rb_commons/schemes/pagination.py,sha256=8VZW1wZGJIPR9jEBUgppZUoB4uqP8ORudHkMwvEJSxg,1866
23
24
  rb_commons/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
25
  rb_commons/utils/media.py,sha256=J2Zi0J28DhcVQVzt-myNNVuzj9Msaetul53VjZtdDdc,820
25
- rb_commons-0.7.3.dist-info/METADATA,sha256=5GCH4rFktZE2zHbYDBS7T6NaaQJRoo-jFeEM0QDsJ7Q,6570
26
- rb_commons-0.7.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
- rb_commons-0.7.3.dist-info/top_level.txt,sha256=HPx_WAYo3_fbg1WCeGHsz3wPGio1ucbnrlm2lmqlJog,11
28
- rb_commons-0.7.3.dist-info/RECORD,,
26
+ rb_commons-0.7.5.dist-info/METADATA,sha256=zwn9BpfVaH7ycv10Pi9wkgmKsgC6tvPe3uzcUzu86wo,6570
27
+ rb_commons-0.7.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
+ rb_commons-0.7.5.dist-info/top_level.txt,sha256=HPx_WAYo3_fbg1WCeGHsz3wPGio1ucbnrlm2lmqlJog,11
29
+ rb_commons-0.7.5.dist-info/RECORD,,