python-general-be-lib 0.9.0__py3-none-any.whl → 0.9.1__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.
@@ -317,7 +317,7 @@ class CrudRepository[Entity: Base | Type[Base]]:
317
317
  return True
318
318
 
319
319
  def find(self, session: Session = None, where: dict[str, Any] = None, columns: list[str] = None, exclude_columns: list[str] = None, limit: int = 0, parsing_model: Type[BaseModel] = None, order_by: str = None,
320
- asc: bool = None, **kwhere) -> list[Entity]:
320
+ asc: bool = None, orders_by: dict[str, bool] = None, **kwhere) -> list[Entity]:
321
321
  """Find the records matching the given criteria.
322
322
 
323
323
  The filters can be passed as a ``where`` dictionary or as keyword arguments. Being a
@@ -337,6 +337,11 @@ class CrudRepository[Entity: Base | Type[Base]]:
337
337
  ``None``, no ordering is applied.
338
338
  asc (bool, optional): Ordering direction, ``True`` for ASC and ``False`` for
339
339
  DESC. When ``None``, the default ordering is DESC.
340
+ orders_by (dict[str, bool], optional): Ordering on several columns, as
341
+ ``{column: ascending}``. The clauses follow the insertion order of the
342
+ dictionary. Takes precedence over ``order_by``/``asc``. The keys are
343
+ database column names, unlike ``columns``, which takes Python attribute
344
+ names.
340
345
  **kwhere: Additional filters as keyword arguments.
341
346
 
342
347
  Returns:
@@ -344,22 +349,26 @@ class CrudRepository[Entity: Base | Type[Base]]:
344
349
  given.
345
350
 
346
351
  Raises:
347
- HasNoAttributeException: If a column in ``columns``, ``exclude_columns`` or
348
- ``order_by`` does not exist on the entity.
352
+ HasNoAttributeException: If a column in ``columns``, ``exclude_columns``,
353
+ ``order_by`` or ``orders_by`` does not exist on the entity.
349
354
  """
350
355
 
351
356
  keep_open = False if session is None else True
352
357
  session = self.open_session if session is None else session
353
- entities = self._find(session=session, where=where, columns=columns, exclude_columns=exclude_columns, limit=limit, order_by=order_by, asc=asc, **kwhere)
358
+ if order_by is not None and orders_by is None:
359
+ orders_by = {order_by: asc if asc is not None else False}
360
+ entities = self._find(session=session, where=where, columns=columns, exclude_columns=exclude_columns, limit=limit, orders_by=orders_by, **kwhere)
354
361
  parsing_model = parsing_model if parsing_model is not None else self._retrieve_parsing_model(columns=columns, exclude_columns=exclude_columns)
355
362
  return self._return(session=session, entities=entities, keep_open=keep_open, commit=False, parsing_model=parsing_model)
356
363
 
357
- def _find(self, session: Session, where: dict[str, Any] = None, columns: list[str] = None, exclude_columns: list[str] = None, limit: int = 0, order_by: str = None,
358
- asc: bool = None, **kwhere):
364
+ def _find(self, session: Session, where: dict[str, Any] = None, columns: list[str] = None, exclude_columns: list[str] = None, limit: int = 0, orders_by: dict[str, bool] = None, **kwhere):
359
365
  if where is None:
360
366
  where = kwhere if kwhere else {}
367
+ if orders_by is None:
368
+ orders_by = {}
361
369
  stmt = self._select(entity=self.entity, columns=columns, exclude_columns=exclude_columns)
362
- stmt = self.stmt_manager.order_by_condition(stmt=stmt, order_by=order_by, asc=asc)
370
+ for order_by, asc in orders_by.items():
371
+ stmt = self.stmt_manager.order_by_condition(stmt=stmt, order_by=order_by, asc=asc)
363
372
  stmt = self.stmt_manager.limit_offset_condition(stmt=stmt, limit=limit)
364
373
 
365
374
  stmt = self.stmt_manager.prepare_statement(stmt=stmt, **where)
@@ -626,7 +635,7 @@ class CrudRepository[Entity: Base | Type[Base]]:
626
635
  return session.execute(stmt).scalar()
627
636
 
628
637
  def paging_find(self, session: Session = None, where: dict[str, Any] = None, columns: list[str] = None, exclude_columns: list[str] = None, limit: int = 0, page: int = 1, order_by: str = None, asc: bool = True,
629
- parsing_model: Type[BaseModel] = None, **kwhere) -> PaginatorResponseModel[Entity]:
638
+ parsing_model: Type[BaseModel] = None, orders_by: dict[str, bool] = None, **kwhere) -> PaginatorResponseModel[Entity]:
630
639
  """Run a paginated search with ordering and offset support.
631
640
 
632
641
  Computes the total number of pages and returns the requested page only. The data
@@ -650,30 +659,40 @@ class CrudRepository[Entity: Base | Type[Base]]:
650
659
  asc (bool): Ascending order if ``True``. Default: ``True``.
651
660
  parsing_model (Type[BaseModel], optional): Alternative model for the
652
661
  serialization.
662
+ orders_by (dict[str, bool], optional): Ordering on several columns, as
663
+ ``{column: ascending}``, with the clauses following the insertion order of
664
+ the dictionary. Takes precedence over ``order_by``/``asc``. The primary key
665
+ is **not** appended as a tie-breaker, so an ordering on a non-unique column
666
+ does not give a total order and two pages can overlap: pass the key
667
+ explicitly as the last entry when the ordering must be stable.
653
668
  **kwhere: Filters as keyword arguments.
654
669
 
655
670
  Returns:
656
671
  PaginatorResponseModel[Entity]: Object carrying ``data``, ``tot_pages``,
657
- ``page``, ``limit``, ``order_by`` and ``asc``.
672
+ ``page``, ``limit``, ``orders_by``, plus ``order_by`` and ``asc`` holding
673
+ the first pair of the ordering.
658
674
  """
659
675
  keep_open = False if session is None else True
660
676
  session = self.open_session if session is None else session
661
- paginated = self._paging_find(session=session, where=where, columns=columns, exclude_columns=exclude_columns, limit=limit, page=page, order_by=order_by, asc=asc, **kwhere)
677
+ orders_by = orders_by if orders_by else {order_by or self.entity.primary_key()[0].key: asc if asc is not None else False}
678
+ paginated = self._paging_find(session=session, where=where, columns=columns, exclude_columns=exclude_columns, limit=limit, page=page, orders_by=orders_by, **kwhere)
662
679
  parsing_model = parsing_model if parsing_model is not None else self._retrieve_parsing_model(columns=columns, exclude_columns=exclude_columns)
663
680
  if not keep_open:
664
681
  paginated.data = _list_adapter(parsing_model).validate_python(paginated.data, from_attributes=True)
665
682
  session.close()
666
683
  return paginated
667
684
 
668
- def _paging_find(self, session: Session, where: dict[str, Any] = None, columns: list[str] = None, exclude_columns: list[str] = None, limit: int = 0, page: int = 1, order_by: str = None, asc: bool = True, **kwhere):
685
+ def _paging_find(self, session: Session, where: dict[str, Any] = None, columns: list[str] = None, exclude_columns: list[str] = None, limit: int = 0, page: int = 1, orders_by: dict[str, bool] = None, **kwhere):
669
686
  if where is None:
670
687
  where = kwhere if kwhere else {}
688
+ if not orders_by:
689
+ orders_by = {self.entity.primary_key()[0].key: True}
671
690
 
672
691
  stmt = self._select(entity=self.entity, columns=columns, exclude_columns=exclude_columns)
673
692
  stmt = self.stmt_manager.limit_offset_condition(stmt=stmt, limit=limit, page=page)
674
693
 
675
- order_by = order_by or self.entity.primary_key()[0].name
676
- stmt = self.stmt_manager.order_by_condition(stmt=stmt, order_by=order_by, asc=asc)
694
+ for order_by, asc in orders_by.items():
695
+ stmt = self.stmt_manager.order_by_condition(stmt=stmt, order_by=order_by, asc=asc)
677
696
 
678
697
  conditions = self.stmt_manager.conditions(where=where)
679
698
  if conditions:
@@ -690,20 +709,22 @@ class CrudRepository[Entity: Base | Type[Base]]:
690
709
  else:
691
710
  count_ = self._preconf_count(session=session, where=conditions)
692
711
 
693
- return self.parse_pagination(limit=limit, page=page, order_by=order_by, count=count_, asc=asc, data=entities)
712
+ return self.parse_pagination(limit=limit, page=page, orders_by=orders_by, count=count_, data=entities)
694
713
 
695
- def parse_pagination(self, limit: int, page: int, order_by: str, asc: bool, count: int, data: Sequence[Row] | Sequence[Row | RowMapping] | Sequence[RowMapping]) -> PaginatorResponseModel:
714
+ def parse_pagination(self, limit: int, page: int, orders_by: dict[str, bool], count: int, data: Sequence[Row] | Sequence[Row | RowMapping] | Sequence[RowMapping]) -> PaginatorResponseModel:
696
715
  """Build the ``PaginatorResponseModel`` from the pagination data.
697
716
 
698
717
  Computes the total number of pages. When ``limit`` is 0, meaning no limit, the
699
- length of the data is used as the effective limit; when ``order_by`` is not given,
700
- the primary key is used as the default ordering field.
718
+ length of the data is used as the effective limit. The response carries the whole
719
+ ``orders_by`` plus its first pair as ``order_by``/``asc``, which are the fields the
720
+ clients read as the main ordering criterion.
701
721
 
702
722
  Args:
703
723
  limit (int): Number of items per page.
704
724
  page (int): Current page.
705
- order_by (str): Ordering field used.
706
- asc (bool): Ordering direction.
725
+ orders_by (dict[str, bool]): Ordering applied, as ``{column: ascending}``. Its
726
+ first pair fills ``order_by``/``asc``; when empty both stay ``None``,
727
+ because no ordering was applied.
707
728
  count (int): Total number of filtered records.
708
729
  data (Sequence): Entities of the current page.
709
730
 
@@ -711,10 +732,11 @@ class CrudRepository[Entity: Base | Type[Base]]:
711
732
  PaginatorResponseModel: Complete paginated response.
712
733
  """
713
734
  limit = limit if limit else len(data)
714
- order_by = order_by if order_by else self.entity.primary_key()[0].name
735
+ order_by = next(iter(orders_by), None) if orders_by else None
736
+ asc = orders_by.get(order_by) if order_by is not None else None
715
737
  tot_pages = math.ceil(count / limit) if count and limit > 0 else 1
716
738
 
717
- return PaginatorResponseModel(limit=limit, page=page, order_by=order_by, asc=asc, tot_pages=tot_pages, data=data)
739
+ return PaginatorResponseModel(limit=limit, page=page, order_by=order_by, asc=asc, tot_pages=tot_pages, orders_by=orders_by, data=data)
718
740
 
719
741
  def distinct_pairs(self, key: str, value: str, session: Session = None, where: dict[str, Any] = None, **kwhere) -> list[Row] | None:
720
742
  """Return the distinct ``(key, value)`` pairs of the records matching ``where``.
@@ -113,12 +113,19 @@ class StatementManager:
113
113
  return stmt
114
114
 
115
115
  def order_by_condition(self, stmt: Select, order_by: str, asc: bool):
116
- """Add the ORDER BY clause to the statement.
116
+ """Add one ORDER BY clause to the statement.
117
+
118
+ Adds a single criterion and can be called repeatedly to build an ordering on
119
+ several columns: ``Select.order_by`` appends, so the clauses keep the order of the
120
+ calls. The column name is resolved against the table columns, so a name that is
121
+ not a column cannot reach the SQL.
117
122
 
118
123
  Args:
119
124
  stmt (Select): Select statement to add the ordering to.
120
- order_by (str): Name of the column to order by.
121
- asc (bool): If ``True``, ascending order (ASC); if ``False``, descending (DESC).
125
+ order_by (str): Database name of the column to order by. Falsy values leave the
126
+ statement untouched.
127
+ asc (bool): If ``True``, ascending order (ASC); if ``False`` or ``None``,
128
+ descending (DESC).
122
129
 
123
130
  Returns:
124
131
  Select: Statement with the ORDER BY clause added.
general/paginator_dto.py CHANGED
@@ -18,12 +18,12 @@ class PaginatorBaseModel(ExtBaseModel):
18
18
  page (int): Current page number, 1-based. Alias: ``numeroPagina``.
19
19
  limit (int | None): Number of items per page. Alias: ``numeroElementi``.
20
20
  order_by (str | None): Field used for the ordering. Alias: ``campoOrdinamento``.
21
- asc (bool): Ascending order if ``True``. Alias: ``crescente``.
21
+ asc (bool | None): Ascending order if ``True``. Alias: ``crescente``.
22
22
  """
23
23
  page: int = Field(alias="numeroPagina")
24
24
  limit: Optional[int] = Field(default=None, alias="numeroElementi")
25
25
  order_by: Optional[str] = Field(default=None, alias="campoOrdinamento")
26
- asc: bool = Field(alias="crescente")
26
+ asc: Optional[bool] = Field(default=None, alias="crescente")
27
27
 
28
28
 
29
29
  class PaginatorRequestModel(PaginatorBaseModel):
@@ -45,8 +45,12 @@ class PaginatorResponseModel[ResponseType](PaginatorBaseModel):
45
45
  Attributes:
46
46
  tot_pages (int): Total number of pages, computed by the repository.
47
47
  Alias: ``pagineTotali``.
48
+ orders_by (dict[str, bool] | None): Whole ordering applied, as
49
+ ``{column: ascending}``. The inherited ``order_by``/``asc`` carry its first
50
+ pair, which is the main criterion. Alias: ``campiOrdinamento``.
48
51
  data (list[ResponseType]): Items of the current page.
49
52
  """
50
53
 
51
54
  tot_pages: int = Field(alias="pagineTotali")
55
+ orders_by: Optional[dict[str, bool]] = Field(default=None, alias="campiOrdinamento")
52
56
  data: list[ResponseType]
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-general-be-lib
3
- Version: 0.9.0
3
+ Version: 0.9.1
4
4
  Summary: General purpose backend library — SQLAlchemy CRUD/Geometry repositories, FastAPI exceptions, Pydantic base models, logger utilities.
5
5
  Author-email: Andrea Di Placido <a.diplacido@arpes.it>, "Arpes S.r.l." <it.admin@arpes.it>
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/Arpes-IS/python-general-be-lib
8
8
  Project-URL: Repository, https://github.com/Arpes-IS/python-general-be-lib
9
+ Project-URL: Changelog, https://github.com/Arpes-IS/python-general-be-lib/blob/main/CHANGELOG.md
9
10
  Keywords: backend,sqlalchemy,fastapi,pydantic,repository,crud,geoalchemy2
10
11
  Classifier: Development Status :: 3 - Alpha
11
12
  Classifier: Intended Audience :: Developers
@@ -1,6 +1,6 @@
1
1
  general/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  general/logger.py,sha256=P-7-BLtO1XC2m43wXrnJcTakr2iZBE0gUkMVppJ9exE,4506
3
- general/paginator_dto.py,sha256=uHMC5fEPZL9It8xY3g4JdGzoH4DXNmrff00DOOgtsgc,1912
3
+ general/paginator_dto.py,sha256=ug2KJMPQRgxcwTu8-MvuLi7MwvmkHDG9cHwfp6EzqIg,2267
4
4
  general/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  general/exception/__init__.py,sha256=3o5KaoCwA020aG0GIR0jOA4t_ibOBiji0xMxZx4uNug,745
6
6
  general/exception/access_exceptions.py,sha256=tNIBjxM0-5iSOXDc7Il01eb36mEk-POQGVaHhCpG278,744
@@ -14,7 +14,7 @@ general/interface/metadata/__init__.py,sha256=pXKYY4uO92eQI3gtuG4FKZbBi76gu-VJLx
14
14
  general/interface/metadata/crud_metadata.py,sha256=i6ldkyUqjHPerninf--sQlGyUm18FgzIBmDq55UYz0g,22819
15
15
  general/interface/metadata/geom_metadata.py,sha256=thfVc0hyKgkTFIQrRaX6khf_NbEHyEkc-zUA7nO8bI8,11918
16
16
  general/interface/repository/__init__.py,sha256=eWoNy35LVu6sDrK5ju7VAeoclknKO0-yTgjVckJpi8s,321
17
- general/interface/repository/crud_repository.py,sha256=r9xq8YTS_GKKhnVPLPdajzo04ID2v0L9ajSb3bNknvI,38288
17
+ general/interface/repository/crud_repository.py,sha256=CH05WG6ZKkDpS5DEB_718AoRqJUS47CMXM65bI-1nCE,40032
18
18
  general/interface/repository/geometry_repository.py,sha256=dB1AmZyyi5cWEzl-_4-2eOXXex-rQsa_cu6bl85wtNE,11252
19
19
  general/interface/repository/many_to_many_repository.py,sha256=wl-K0n-rwPvIDpa_LfoBq1iHUnC6QvSLm-uwyKA4taw,8574
20
20
  general/interface/repository/view_repository.py,sha256=wxDGw7kenuVrDd9nfh7-omw2m9Jex5WzF_lhEiqPoA0,7902
@@ -26,9 +26,9 @@ general/interface/repository/handler/interval_handler.py,sha256=4TtEcpzUFZeyB-yi
26
26
  general/interface/repository/handler/json_handler.py,sha256=f_nGemsxTPJ3W0xkU99sCFA0EVo1xa8p803NyjcGJT4,4414
27
27
  general/interface/repository/handler/json_ilike_handler.py,sha256=icfA4dO8E_W_KUEG5HB1nLVI1jfjxzkG8ZJ-IaCd4kY,3530
28
28
  general/interface/repository/handler/or_handler.py,sha256=UOjIh77H4y-x434YGLYRfVIXKnod1M5XPiMf71V2JTg,3137
29
- general/interface/repository/handler/statement_manager.py,sha256=U0jQNAV33ihwHwotIUa3kgqsV9Kofi_t2Te4TDFb0WM,6928
30
- python_general_be_lib-0.9.0.dist-info/licenses/LICENSE,sha256=iUaO1XZyB9P3Tmog0OILuTisP6vXGe3QKz-4yRTxOFk,1069
31
- python_general_be_lib-0.9.0.dist-info/METADATA,sha256=RgbrOLnOUhFXi95YVLFVZvDUxxu8dSfittwi8AjB5NA,3312
32
- python_general_be_lib-0.9.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
33
- python_general_be_lib-0.9.0.dist-info/top_level.txt,sha256=tTZePW8_CNUqSgKFd2SEH72ZbnhS0OYjRsgcv0ikSFY,8
34
- python_general_be_lib-0.9.0.dist-info/RECORD,,
29
+ general/interface/repository/handler/statement_manager.py,sha256=dPG-HZy0jVM3Y4ZnCp2QPY4gLgt-rWSzOvY1_UmotcI,7333
30
+ python_general_be_lib-0.9.1.dist-info/licenses/LICENSE,sha256=iUaO1XZyB9P3Tmog0OILuTisP6vXGe3QKz-4yRTxOFk,1069
31
+ python_general_be_lib-0.9.1.dist-info/METADATA,sha256=J4CIjmhDsD6SN26tAfFUvtbd2eQOggjhucncPn6Sn6c,3409
32
+ python_general_be_lib-0.9.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
33
+ python_general_be_lib-0.9.1.dist-info/top_level.txt,sha256=tTZePW8_CNUqSgKFd2SEH72ZbnhS0OYjRsgcv0ikSFY,8
34
+ python_general_be_lib-0.9.1.dist-info/RECORD,,