rb-commons 0.7.14__py3-none-any.whl → 0.7.16__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 +143 -16
- {rb_commons-0.7.14.dist-info → rb_commons-0.7.16.dist-info}/METADATA +1 -1
- {rb_commons-0.7.14.dist-info → rb_commons-0.7.16.dist-info}/RECORD +5 -5
- {rb_commons-0.7.14.dist-info → rb_commons-0.7.16.dist-info}/WHEEL +0 -0
- {rb_commons-0.7.14.dist-info → rb_commons-0.7.16.dist-info}/top_level.txt +0 -0
rb_commons/orm/managers.py
CHANGED
@@ -43,6 +43,16 @@ def query_mutator(func: F) -> F:
|
|
43
43
|
return wrapper
|
44
44
|
|
45
45
|
|
46
|
+
AGG_MAP: dict[str, Callable[[Any], Any]] = {
|
47
|
+
"sum": func.sum,
|
48
|
+
"avg": func.avg,
|
49
|
+
"mean": func.avg,
|
50
|
+
"min": func.min,
|
51
|
+
"max": func.max,
|
52
|
+
"count": func.count,
|
53
|
+
"first": lambda c: c,
|
54
|
+
}
|
55
|
+
|
46
56
|
class BaseManager(Generic[ModelType]):
|
47
57
|
model: Type[ModelType]
|
48
58
|
|
@@ -126,6 +136,34 @@ class BaseManager(Generic[ModelType]):
|
|
126
136
|
|
127
137
|
def _parse_lookup(self, lookup: str, value: Any):
|
128
138
|
parts, operator, rel_attr, col_attr = self._parse_lookup_meta(lookup)
|
139
|
+
|
140
|
+
if rel_attr is not None and col_attr is None:
|
141
|
+
uselist = rel_attr.property.uselist
|
142
|
+
primaryjoin = rel_attr.property.primaryjoin
|
143
|
+
|
144
|
+
if uselist:
|
145
|
+
target_cls = rel_attr.property.mapper.class_
|
146
|
+
cnt = (
|
147
|
+
select(func.count("*"))
|
148
|
+
.select_from(target_cls)
|
149
|
+
.where(primaryjoin)
|
150
|
+
.correlate(self.model)
|
151
|
+
.scalar_subquery()
|
152
|
+
)
|
153
|
+
return self._build_comparison(cnt, operator, value)
|
154
|
+
else:
|
155
|
+
exists_expr = (
|
156
|
+
select(1)
|
157
|
+
.where(primaryjoin)
|
158
|
+
.correlate(self.model)
|
159
|
+
.exists()
|
160
|
+
)
|
161
|
+
if operator in {"eq", "lte"} and str(value) in {"0", "False", "false"}:
|
162
|
+
return ~exists_expr
|
163
|
+
if operator in {"gt", "gte", "eq"} and str(value) in {"1", "True", "true"}:
|
164
|
+
return exists_expr
|
165
|
+
return self._build_comparison(exists_expr, operator, bool(value))
|
166
|
+
|
129
167
|
expr = self._build_comparison(col_attr, operator, value)
|
130
168
|
|
131
169
|
if rel_attr:
|
@@ -525,27 +563,116 @@ class BaseManager(Generic[ModelType]):
|
|
525
563
|
self._filtered = True
|
526
564
|
return self
|
527
565
|
|
528
|
-
|
529
|
-
|
566
|
+
def _infer_default_agg(self, column) -> str:
|
567
|
+
try:
|
568
|
+
from sqlalchemy import Integer, BigInteger, SmallInteger, Float, Numeric
|
569
|
+
if hasattr(column, "type") and isinstance(column.type, (Integer, BigInteger, SmallInteger, Float, Numeric)):
|
570
|
+
return "sum"
|
571
|
+
except Exception:
|
572
|
+
pass
|
573
|
+
return "max"
|
574
|
+
def _order_expr_for_path(self, token: str):
|
530
575
|
"""
|
531
|
-
|
532
|
-
|
576
|
+
token grammar:
|
577
|
+
[-]<path>[:<agg>][!first|!last]
|
578
|
+
<path> := "field" | "relation__field" (one hop)
|
579
|
+
<agg> := sum|avg|min|max|count|first (required for uselist=True; optional otherwise)
|
580
|
+
Examples:
|
581
|
+
"category__title"
|
582
|
+
"-reviews__rating:avg!last"
|
583
|
+
"stocks__sold:sum"
|
533
584
|
"""
|
534
|
-
self._invalid_sort_tokens = []
|
535
|
-
self._order_by = []
|
536
|
-
model = self.model
|
537
585
|
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
586
|
+
# strip leading '-' (handled by caller), and parse nulls placement
|
587
|
+
core = token.lstrip("-")
|
588
|
+
nulls_placement = None
|
589
|
+
if core.endswith("!first"):
|
590
|
+
core, nulls_placement = core[:-6], "first"
|
591
|
+
elif core.endswith("!last"):
|
592
|
+
core, nulls_placement = core[:-5], "last"
|
593
|
+
|
594
|
+
# split aggregate suffix if present
|
595
|
+
if ":" in core:
|
596
|
+
path, agg_name = core.split(":", 1)
|
597
|
+
agg_name = agg_name.lower().strip()
|
598
|
+
else:
|
599
|
+
path, agg_name = core, None
|
600
|
+
|
601
|
+
# base column on the model (no relation hop)
|
602
|
+
if "__" not in path and "." not in path:
|
603
|
+
col = getattr(self.model, path, None)
|
544
604
|
if col is None:
|
545
|
-
self.
|
546
|
-
|
547
|
-
|
605
|
+
raise ValueError(f"Invalid order_by field '{path}' for {self.model.__name__}")
|
606
|
+
expr = col
|
607
|
+
if nulls_placement == "first":
|
608
|
+
expr = expr.nullsfirst()
|
609
|
+
elif nulls_placement == "last":
|
610
|
+
expr = expr.nullslast()
|
611
|
+
return expr
|
612
|
+
|
613
|
+
# relation hop (exactly one)
|
614
|
+
parts = re.split(r"\.|\_\_", path)
|
615
|
+
if len(parts) != 2:
|
616
|
+
raise ValueError(f"Only one relation hop supported in order_by: {path!r}")
|
617
|
+
|
618
|
+
rel_name, col_name = parts
|
619
|
+
rel_attr = getattr(self.model, rel_name, None)
|
620
|
+
if rel_attr is None or not hasattr(rel_attr, "property"):
|
621
|
+
raise ValueError(f"Invalid relationship '{rel_name}' on {self.model.__name__}")
|
622
|
+
|
623
|
+
target_mapper = rel_attr.property.mapper
|
624
|
+
target_cls = target_mapper.class_
|
625
|
+
target_col = getattr(target_cls, col_name, None)
|
626
|
+
if target_col is None:
|
627
|
+
raise ValueError(f"Invalid column '{col_name}' on related model {target_cls.__name__}")
|
628
|
+
|
629
|
+
primaryjoin = rel_attr.property.primaryjoin
|
630
|
+
uselist = rel_attr.property.uselist
|
631
|
+
|
632
|
+
# One-to-many (or many-to-many via association): require aggregate (or infer)
|
633
|
+
if uselist:
|
634
|
+
agg_name = agg_name or self._infer_default_agg(target_col)
|
635
|
+
agg_fn = AGG_MAP.get(agg_name)
|
636
|
+
if agg_fn is None:
|
637
|
+
raise ValueError(f"Unsupported aggregate '{agg_name}' in order_by for {path!r}")
|
638
|
+
|
639
|
+
# SELECT agg(related.col) WHERE primaryjoin (correlated)
|
640
|
+
subq = (
|
641
|
+
select(agg_fn(target_col))
|
642
|
+
.where(primaryjoin)
|
643
|
+
.correlate(self.model) # tie to outer row
|
644
|
+
.scalar_subquery()
|
645
|
+
)
|
646
|
+
expr = subq
|
647
|
+
|
648
|
+
else:
|
649
|
+
if agg_name and agg_name != "first":
|
650
|
+
agg_fn = AGG_MAP.get(agg_name)
|
651
|
+
if agg_fn is None:
|
652
|
+
raise ValueError(f"Unsupported aggregate '{agg_name}' in order_by for {path!r}")
|
653
|
+
select_expr = agg_fn(target_col)
|
654
|
+
else:
|
655
|
+
select_expr = target_col
|
656
|
+
|
657
|
+
sub = select(select_expr).where(primaryjoin).correlate(self.model)
|
658
|
+
if agg_name == "first":
|
659
|
+
sub = sub.limit(1)
|
660
|
+
expr = sub.scalar_subquery()
|
661
|
+
|
662
|
+
if nulls_placement == "first":
|
663
|
+
expr = expr.nullsfirst()
|
664
|
+
elif nulls_placement == "last":
|
665
|
+
expr = expr.nullslast()
|
548
666
|
|
667
|
+
return expr
|
668
|
+
|
669
|
+
@query_mutator
|
670
|
+
def sort_by(self, tokens):
|
671
|
+
self._order_by = []
|
672
|
+
for tok in tokens or []:
|
673
|
+
direction = desc if tok.startswith("-") else asc
|
674
|
+
name = tok.lstrip("-")
|
675
|
+
self._order_by.append(direction(self._order_expr_for_path(name)))
|
549
676
|
return self
|
550
677
|
|
551
678
|
def model_to_dict(self, instance: ModelType, exclude: set[str] = None) -> dict:
|
@@ -14,7 +14,7 @@ rb_commons/http/exceptions.py,sha256=EGRMr1cRgiJ9Q2tkfANbf0c6-zzXf1CD6J3cmCaT_FA
|
|
14
14
|
rb_commons/orm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
15
|
rb_commons/orm/enum.py,sha256=PRNSuP1X7ADJW1snnxRGcHj9mxE3JaO-a2AyaHmB6es,380
|
16
16
|
rb_commons/orm/exceptions.py,sha256=1aMctiEwrPjyehoXVX1l6ML5ZOhmDkmBISzlTD5ey1Y,509
|
17
|
-
rb_commons/orm/managers.py,sha256=
|
17
|
+
rb_commons/orm/managers.py,sha256=SSKeIy8t0Qw7SdmbAqqz3uAMNIs4ePgkINyg0ch6X0w,25308
|
18
18
|
rb_commons/orm/querysets.py,sha256=Q4iY_TKZItpNnrbCAvlSwyCxJikhaX5qVF3Y2rVgAYQ,1666
|
19
19
|
rb_commons/orm/services.py,sha256=71eRcJ4TxZvzNz-hLXo12X4U7PGK54ZfbLAb27AjZi8,1589
|
20
20
|
rb_commons/permissions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -24,7 +24,7 @@ rb_commons/schemes/jwt.py,sha256=ZKLJ5D3fcEmEKySjzbxEgUcza4K-oPoHr14_Z0r9Yic,249
|
|
24
24
|
rb_commons/schemes/pagination.py,sha256=8VZW1wZGJIPR9jEBUgppZUoB4uqP8ORudHkMwvEJSxg,1866
|
25
25
|
rb_commons/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
26
26
|
rb_commons/utils/media.py,sha256=pwwGyImI5BK-NCJkX0Q6w2Nm-QL9_CCQC7B7O7wz38I,1493
|
27
|
-
rb_commons-0.7.
|
28
|
-
rb_commons-0.7.
|
29
|
-
rb_commons-0.7.
|
30
|
-
rb_commons-0.7.
|
27
|
+
rb_commons-0.7.16.dist-info/METADATA,sha256=33zDYBVoHrYifxF8AM1tzUuIKRmnRMwW4DqWbf3ddCM,6571
|
28
|
+
rb_commons-0.7.16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
29
|
+
rb_commons-0.7.16.dist-info/top_level.txt,sha256=HPx_WAYo3_fbg1WCeGHsz3wPGio1ucbnrlm2lmqlJog,11
|
30
|
+
rb_commons-0.7.16.dist-info/RECORD,,
|
File without changes
|
File without changes
|