rb-commons 0.7.14__tar.gz → 0.7.16__tar.gz

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.
Files changed (35) hide show
  1. {rb_commons-0.7.14 → rb_commons-0.7.16}/PKG-INFO +1 -1
  2. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons/orm/managers.py +143 -16
  3. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons.egg-info/PKG-INFO +1 -1
  4. {rb_commons-0.7.14 → rb_commons-0.7.16}/setup.py +1 -1
  5. {rb_commons-0.7.14 → rb_commons-0.7.16}/README.md +0 -0
  6. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons/__init__.py +0 -0
  7. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons/broker/__init__.py +0 -0
  8. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons/broker/consumer.py +0 -0
  9. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons/configs/__init__.py +0 -0
  10. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons/configs/config.py +0 -0
  11. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons/configs/injections.py +0 -0
  12. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons/configs/rabbitmq.py +0 -0
  13. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons/configs/v2/__init__.py +0 -0
  14. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons/configs/v2/config.py +0 -0
  15. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons/http/__init__.py +0 -0
  16. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons/http/base_api.py +0 -0
  17. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons/http/consul.py +0 -0
  18. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons/http/exceptions.py +0 -0
  19. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons/orm/__init__.py +0 -0
  20. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons/orm/enum.py +0 -0
  21. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons/orm/exceptions.py +0 -0
  22. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons/orm/querysets.py +0 -0
  23. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons/orm/services.py +0 -0
  24. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons/permissions/__init__.py +0 -0
  25. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons/permissions/role_permissions.py +0 -0
  26. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons/schemes/__init__.py +0 -0
  27. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons/schemes/jwt.py +0 -0
  28. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons/schemes/pagination.py +0 -0
  29. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons/utils/__init__.py +0 -0
  30. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons/utils/media.py +0 -0
  31. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons.egg-info/SOURCES.txt +0 -0
  32. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons.egg-info/dependency_links.txt +0 -0
  33. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons.egg-info/requires.txt +0 -0
  34. {rb_commons-0.7.14 → rb_commons-0.7.16}/rb_commons.egg-info/top_level.txt +0 -0
  35. {rb_commons-0.7.14 → rb_commons-0.7.16}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rb-commons
3
- Version: 0.7.14
3
+ Version: 0.7.16
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
@@ -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
- @query_mutator
529
- def sort_by(self, tokens: Sequence[str]) -> "BaseManager[ModelType]":
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
- Dynamically apply ORDER BY clauses based on a list of "field" or "-field" tokens.
532
- Unknown fields are collected for Python-side sorting later.
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
- for tok in tokens:
539
- if not tok:
540
- continue
541
- direction = desc if tok.startswith("-") else asc
542
- name = tok.lstrip("-")
543
- col = getattr(model, name, None)
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._invalid_sort_tokens.append(tok)
546
- continue
547
- self._order_by.append(direction(col))
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rb-commons
3
- Version: 0.7.14
3
+ Version: 0.7.16
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
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
5
5
 
6
6
  setup(
7
7
  name="rb-commons",
8
- version="0.7.14",
8
+ version="0.7.16",
9
9
  author="Abdulvoris",
10
10
  author_email="erkinovabdulvoris101@gmail.com",
11
11
  description="Commons of project and simplified orm based on sqlalchemy.",
File without changes
File without changes