python-general-be-lib 0.12.1__py3-none-any.whl → 0.12.2__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.
- general/interface/metadata/crud_metadata.py +16 -8
- general/interface/repository/crud_repository.py +32 -8
- general/paginator_dto.py +30 -4
- {python_general_be_lib-0.12.1.dist-info → python_general_be_lib-0.12.2.dist-info}/METADATA +1 -1
- {python_general_be_lib-0.12.1.dist-info → python_general_be_lib-0.12.2.dist-info}/RECORD +8 -8
- {python_general_be_lib-0.12.1.dist-info → python_general_be_lib-0.12.2.dist-info}/WHEEL +0 -0
- {python_general_be_lib-0.12.1.dist-info → python_general_be_lib-0.12.2.dist-info}/licenses/LICENSE +0 -0
- {python_general_be_lib-0.12.1.dist-info → python_general_be_lib-0.12.2.dist-info}/top_level.txt +0 -0
|
@@ -11,7 +11,7 @@ from sqlalchemy.sql.functions import count
|
|
|
11
11
|
from ...exception.crud_exceptions import BadRequestException, EntityNotFoundException, HasNoAttributeException, InternalServerException, raise_http_exception
|
|
12
12
|
from ...interface.repository.handler.base_handler import BaseHandler
|
|
13
13
|
from ...interface.repository.handler.statement_manager import StatementManager
|
|
14
|
-
from ...paginator_dto import PaginatorResponseModel, parse_pagination
|
|
14
|
+
from ...paginator_dto import PaginatorResponseModel, parse_pagination, _with_tiebreaker
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class CrudMetadata:
|
|
@@ -865,14 +865,17 @@ class CrudMetadata:
|
|
|
865
865
|
page (int): Requested page number, 1-based. Default: 1.
|
|
866
866
|
parsing_model (Type[BaseModel], optional): Model each row is serialized with.
|
|
867
867
|
orders_by (dict[str, bool], optional): Ordering, as ``{column: ascending}``,
|
|
868
|
-
with the clauses following the insertion order of the dictionary.
|
|
869
|
-
|
|
870
|
-
overlap
|
|
868
|
+
with the clauses following the insertion order of the dictionary. The
|
|
869
|
+
primary key columns missing from it are appended, ascending, as a
|
|
870
|
+
tie-breaker, so that two pages cannot overlap; a key column already
|
|
871
|
+
passed keeps its position and direction. A table without a primary key,
|
|
872
|
+
such as a reflected view, gets no tie-breaker. When omitted no ordering
|
|
873
|
+
is applied at all, and the pages are not stable.
|
|
871
874
|
**kwhere: Additional filters as keyword arguments.
|
|
872
875
|
|
|
873
876
|
Returns:
|
|
874
877
|
PaginatorResponseModel: Object carrying ``data``, ``tot_pages``, ``page``,
|
|
875
|
-
``limit`` and the ordering.
|
|
878
|
+
``limit`` and the ordering requested, without the tie-breaker.
|
|
876
879
|
|
|
877
880
|
Raises:
|
|
878
881
|
HasNoAttributeException: If the table or one of the columns does not exist.
|
|
@@ -891,6 +894,9 @@ class CrudMetadata:
|
|
|
891
894
|
orders_by: dict[str, bool] = None, **kwhere) -> PaginatorResponseModel:
|
|
892
895
|
"""Read the page and derive the total, running the ``COUNT`` only when needed.
|
|
893
896
|
|
|
897
|
+
A non-empty ordering gets the missing primary key columns appended, ascending, so
|
|
898
|
+
that it is total; the response reports it as requested, without them.
|
|
899
|
+
|
|
894
900
|
Args:
|
|
895
901
|
table (Table): Table to search on.
|
|
896
902
|
connection (Connection): Connection to run the statements on.
|
|
@@ -898,7 +904,8 @@ class CrudMetadata:
|
|
|
898
904
|
columns (list[str], optional): Columns to select.
|
|
899
905
|
limit (int): Rows per page. 0 means no limit.
|
|
900
906
|
page (int): Page number, 1-based.
|
|
901
|
-
orders_by (dict[str, bool], optional): Ordering
|
|
907
|
+
orders_by (dict[str, bool], optional): Ordering requested. When empty,
|
|
908
|
+
nothing is ordered.
|
|
902
909
|
**kwhere: Additional filters as keyword arguments.
|
|
903
910
|
|
|
904
911
|
Returns:
|
|
@@ -911,8 +918,9 @@ class CrudMetadata:
|
|
|
911
918
|
|
|
912
919
|
manager = self._statement_manager(table)
|
|
913
920
|
stmt = self._select(table=table, columns=columns)
|
|
914
|
-
|
|
915
|
-
|
|
921
|
+
if orders_by:
|
|
922
|
+
stmt = self._add_orders_by_condition(stmt, table, _with_tiebreaker(orders_by, table.primary_key.columns.keys()))
|
|
923
|
+
stmt = manager.limit_offset_condition(stmt=stmt, limit=limit, page=page)
|
|
916
924
|
conditions = manager.conditions(where=dict(where))
|
|
917
925
|
if conditions:
|
|
918
926
|
stmt = stmt.where(*conditions)
|
|
@@ -16,7 +16,7 @@ from .handler.base_handler import BaseHandler
|
|
|
16
16
|
from .handler.statement_manager import StatementManager
|
|
17
17
|
from ...exception.crud_exceptions import BadRequestException, EntityNotFoundException, HasNoAttributeException, raise_http_exception
|
|
18
18
|
from ...interface.base.declarative_base import Base
|
|
19
|
-
from ...paginator_dto import PaginatorResponseModel, parse_pagination
|
|
19
|
+
from ...paginator_dto import PaginatorResponseModel, parse_pagination, _with_tiebreaker
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
@lru_cache(maxsize=None)
|
|
@@ -885,22 +885,25 @@ class CrudRepository[Entity: Base | Type[Base]]:
|
|
|
885
885
|
therefore a single page.
|
|
886
886
|
page (int): Requested page number, 1-based. Default: 1.
|
|
887
887
|
order_by (str, optional): Column to order by. When ``None``, the first column of
|
|
888
|
-
the primary key is used
|
|
888
|
+
the primary key is used, followed by the other ones if the key is
|
|
889
|
+
composite.
|
|
889
890
|
asc (bool): Ascending order if ``True``. Default: ``True``.
|
|
890
891
|
parsing_model (Type[BaseModel], optional): Alternative model for the
|
|
891
892
|
serialization.
|
|
892
893
|
orders_by (dict[str, bool], optional): Ordering on several columns, as
|
|
893
894
|
``{column: ascending}``, with the clauses following the insertion order of
|
|
894
895
|
the dictionary. Takes precedence over ``order_by``/``asc``. The primary key
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
896
|
+
columns missing from the ordering are appended, ascending, as a
|
|
897
|
+
tie-breaker: an ordering on a non-unique column is not total, and with
|
|
898
|
+
``LIMIT/OFFSET`` two pages could overlap and some rows never show up. A
|
|
899
|
+
key column the caller already passed keeps its position and direction.
|
|
898
900
|
**kwhere: Filters as keyword arguments.
|
|
899
901
|
|
|
900
902
|
Returns:
|
|
901
903
|
PaginatorResponseModel[Entity]: Object carrying ``data``, ``tot_pages``,
|
|
902
904
|
``page``, ``limit``, ``orders_by``, plus ``order_by`` and ``asc`` holding
|
|
903
|
-
the first pair of the ordering.
|
|
905
|
+
the first pair of the ordering. ``orders_by`` is the ordering requested,
|
|
906
|
+
without the appended tie-breaker.
|
|
904
907
|
"""
|
|
905
908
|
keep_open = False if session is None else True
|
|
906
909
|
session = self.open_session if session is None else session
|
|
@@ -913,15 +916,36 @@ class CrudRepository[Entity: Base | Type[Base]]:
|
|
|
913
916
|
return paginated
|
|
914
917
|
|
|
915
918
|
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):
|
|
919
|
+
"""Read the page with a total ordering and derive the total, counting only when needed.
|
|
920
|
+
|
|
921
|
+
The primary key columns missing from ``orders_by`` are appended to the ``ORDER BY``,
|
|
922
|
+
ascending, so that ``LIMIT/OFFSET`` cuts every page from the same sequence of rows.
|
|
923
|
+
The response reports ``orders_by`` as requested, without them.
|
|
924
|
+
|
|
925
|
+
Args:
|
|
926
|
+
session (Session): Session to run the statements on.
|
|
927
|
+
where (dict[str, Any], optional): Filters. When ``None``, ``**kwhere`` is used.
|
|
928
|
+
columns (list[str], optional): Columns to include.
|
|
929
|
+
exclude_columns (list[str], optional): Columns to exclude.
|
|
930
|
+
limit (int): Rows per page. 0 means no limit.
|
|
931
|
+
page (int): Page number, 1-based.
|
|
932
|
+
orders_by (dict[str, bool], optional): Ordering requested, as
|
|
933
|
+
``{column: ascending}``. When empty, the primary key ascending.
|
|
934
|
+
**kwhere: Filters as keyword arguments.
|
|
935
|
+
|
|
936
|
+
Returns:
|
|
937
|
+
PaginatorResponseModel: Page and total, with the raw entities as ``data``.
|
|
938
|
+
"""
|
|
916
939
|
if where is None:
|
|
917
940
|
where = kwhere if kwhere else {}
|
|
941
|
+
primary_key = [column.key for column in self.entity.primary_key()]
|
|
918
942
|
if not orders_by:
|
|
919
|
-
orders_by = {
|
|
943
|
+
orders_by = {primary_key[0]: True}
|
|
920
944
|
|
|
921
945
|
stmt = self._select(entity=self.entity, columns=columns, exclude_columns=exclude_columns)
|
|
922
946
|
stmt = self.stmt_manager.limit_offset_condition(stmt=stmt, limit=limit, page=page)
|
|
923
947
|
|
|
924
|
-
for order_by, asc in orders_by.items():
|
|
948
|
+
for order_by, asc in _with_tiebreaker(orders_by, primary_key).items():
|
|
925
949
|
stmt = self.stmt_manager.order_by_condition(stmt=stmt, order_by=order_by, asc=asc)
|
|
926
950
|
|
|
927
951
|
conditions = self.stmt_manager.conditions(where=where)
|
general/paginator_dto.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
__all__ = ["PaginatorRequestModel", "PaginatorResponseModel", "parse_pagination"]
|
|
2
2
|
|
|
3
3
|
import math
|
|
4
|
-
from typing import Any, Optional, Sequence
|
|
4
|
+
from typing import Any, Iterable, Optional, Sequence
|
|
5
5
|
|
|
6
6
|
from pydantic import Field
|
|
7
7
|
|
|
@@ -46,9 +46,11 @@ class PaginatorResponseModel[ResponseType](PaginatorBaseModel):
|
|
|
46
46
|
Attributes:
|
|
47
47
|
tot_pages (int): Total number of pages, computed by the repository.
|
|
48
48
|
Alias: ``pagineTotali``.
|
|
49
|
-
orders_by (dict[str, bool] | None): Whole ordering
|
|
49
|
+
orders_by (dict[str, bool] | None): Whole ordering requested, as
|
|
50
50
|
``{column: ascending}``. The inherited ``order_by``/``asc`` carry its first
|
|
51
|
-
pair, which is the main criterion.
|
|
51
|
+
pair, which is the main criterion. The primary key columns that
|
|
52
|
+
``paging_find`` appends as a tie-breaker are not listed: they are an
|
|
53
|
+
internal detail of the query. Alias: ``campiOrdinamento``.
|
|
52
54
|
data (list[ResponseType]): Items of the current page.
|
|
53
55
|
"""
|
|
54
56
|
|
|
@@ -70,7 +72,7 @@ def parse_pagination(limit: int, page: int, orders_by: dict[str, bool], count: i
|
|
|
70
72
|
Args:
|
|
71
73
|
limit (int): Number of items per page.
|
|
72
74
|
page (int): Current page.
|
|
73
|
-
orders_by (dict[str, bool]): Ordering
|
|
75
|
+
orders_by (dict[str, bool]): Ordering requested, as ``{column: ascending}``.
|
|
74
76
|
count (int): Total number of filtered records.
|
|
75
77
|
data (Sequence): Rows of the current page.
|
|
76
78
|
|
|
@@ -83,3 +85,27 @@ def parse_pagination(limit: int, page: int, orders_by: dict[str, bool], count: i
|
|
|
83
85
|
tot_pages = math.ceil(count / limit) if count and limit > 0 else 1
|
|
84
86
|
|
|
85
87
|
return PaginatorResponseModel(limit=limit, page=page, order_by=order_by, asc=asc, tot_pages=tot_pages, orders_by=orders_by, data=data)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _with_tiebreaker(orders_by: dict[str, bool], keys: Iterable[str]) -> dict[str, bool]:
|
|
91
|
+
"""Append to an ordering the key columns it does not contain yet, ascending.
|
|
92
|
+
|
|
93
|
+
``LIMIT/OFFSET`` over an ordering that is not total is unstable: the database may
|
|
94
|
+
return the rows tied on the ordering columns in a different order from one query to
|
|
95
|
+
the next, so consecutive pages overlap and some rows are never shown. Appending the
|
|
96
|
+
primary key makes the ordering total. A key column the caller already ordered by is
|
|
97
|
+
left where it is and with its direction, since the ordering is total from that point
|
|
98
|
+
on anyway. Shared by the ORM and the metadata layer.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
orders_by (dict[str, bool]): Ordering as ``{column: ascending}``. Never mutated.
|
|
102
|
+
keys (Iterable[str]): Names of the primary key columns, as the statement
|
|
103
|
+
manager resolves them. Empty when the table has no primary key, in which case
|
|
104
|
+
the ordering is returned as it is.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
dict[str, bool]: ``orders_by`` itself when nothing is missing, otherwise a new
|
|
108
|
+
dictionary with the missing key columns appended as ``True``.
|
|
109
|
+
"""
|
|
110
|
+
missing = [key for key in keys if key not in orders_by]
|
|
111
|
+
return {**orders_by, **dict.fromkeys(missing, True)} if missing else orders_by
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-general-be-lib
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.2
|
|
4
4
|
Summary: General purpose backend library — SQLAlchemy CRUD/Geometry repositories, FastAPI exceptions, Pydantic base models, Keycloak JWT auth, 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
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
general/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
general/context.py,sha256=AbASV-jPnDM-M-Nih5c9vKHFlIS0kjp8Q1yuHC34FeA,1925
|
|
3
3
|
general/logger.py,sha256=MnK8ZDA9x4xhX0KcpOxTieosnFIhFVwSa8b52Q4T9tA,5039
|
|
4
|
-
general/paginator_dto.py,sha256=
|
|
4
|
+
general/paginator_dto.py,sha256=0aIdJIOXJlwoQ3d7w8A6tOZ1An6AWBjoHMhIqtZJhic,5221
|
|
5
5
|
general/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
general/auth/__init__.py,sha256=6cYCA6Ir-nnBK9z_bUfh_io3wEYjJUwHAN8mdhjtKeI,2239
|
|
7
7
|
general/auth/jwt_model.py,sha256=KcopeNUPRSQOLvreoIuSo6kJybRtrmhzLoJ5oHFngGw,3447
|
|
@@ -17,14 +17,14 @@ general/interface/base/__init__.py,sha256=wRKbvb8lF33_2iG100mEQXz1tEkh9BF5bPcIi7
|
|
|
17
17
|
general/interface/base/base_model.py,sha256=aIzrRZVUytmbgmEH1bya29rVFOm-Njinn_JXkgkBrr8,2575
|
|
18
18
|
general/interface/base/declarative_base.py,sha256=J-q6nJIdYixFk9tgWjmGVII2cHeOazf0e4snaO_rHhI,9302
|
|
19
19
|
general/interface/metadata/__init__.py,sha256=CawnfGK7WcJmtqi9CjEjBqCyX_vuHFK-Td1orXTH__U,1849
|
|
20
|
-
general/interface/metadata/crud_metadata.py,sha256=
|
|
20
|
+
general/interface/metadata/crud_metadata.py,sha256=4pK4ptX5w0HYv2DK8sj4cJeVfnGJRz9lAjiWgGvYyhs,48763
|
|
21
21
|
general/interface/metadata/geom_metadata.py,sha256=xWNZnWhN_Om91pcqSsURKi5EUYNZh444zDqMwoOpq84,15508
|
|
22
22
|
general/interface/mixin/__init__.py,sha256=M5-e6T_R63Utwh3_WcmoE3wanwIZjNjbgLYrrFlSpTA,659
|
|
23
23
|
general/interface/mixin/audit_mixin.py,sha256=N2JDD9JxXDMeBqu0yAlhVWFOK3EeUA2b215hPvXytUU,1421
|
|
24
24
|
general/interface/mixin/softdelete_mixin.py,sha256=526luzM4DBCorcq99kZ6BYqtQKKJCpnxX9rSEigfyIU,2148
|
|
25
25
|
general/interface/mixin/timestamp_mixin.py,sha256=wmcIx2Y8jgVqg9A-1E61BvkcKCo52ZjKNHeKfXJmUl0,2220
|
|
26
26
|
general/interface/repository/__init__.py,sha256=QQD6okLfRozC0uOZPGrwOUDKJyZTgpObvwJ3OOSJdbM,1813
|
|
27
|
-
general/interface/repository/crud_repository.py,sha256=
|
|
27
|
+
general/interface/repository/crud_repository.py,sha256=W1kZyMw1VCOhbDYS5YpDBgXez0-0pzy6xFgcbR1Scb0,58469
|
|
28
28
|
general/interface/repository/geometry_repository.py,sha256=Fmv0qhfqumnU5fiETMMAmiB0UyG43b19jlPXQ8cJ9CU,11289
|
|
29
29
|
general/interface/repository/many_to_many_repository.py,sha256=KRJ2SMUGDYNoGV-ea541wlWg0FW4NQ_Ry85aJBarmC8,11183
|
|
30
30
|
general/interface/repository/view_repository.py,sha256=wxDGw7kenuVrDd9nfh7-omw2m9Jex5WzF_lhEiqPoA0,7902
|
|
@@ -40,8 +40,8 @@ general/interface/repository/handler/soft_delete_handler.py,sha256=1WD1n6q5plyJ0
|
|
|
40
40
|
general/interface/repository/handler/statement_manager.py,sha256=JSj6K-iQJ06kLdsMTOqK62cIvR87ioOA1LmLTGpqrB0,8205
|
|
41
41
|
general/utils/__init__.py,sha256=_SPe88reIZ0JxwK7mp-u_p-9c4hCcZ-daUihi0gymNk,103
|
|
42
42
|
general/utils/consts.py,sha256=nE75Ud7sVuDw9K_xmIJNjL_5f_BVJmwGNPGFYc26H3Y,264
|
|
43
|
-
python_general_be_lib-0.12.
|
|
44
|
-
python_general_be_lib-0.12.
|
|
45
|
-
python_general_be_lib-0.12.
|
|
46
|
-
python_general_be_lib-0.12.
|
|
47
|
-
python_general_be_lib-0.12.
|
|
43
|
+
python_general_be_lib-0.12.2.dist-info/licenses/LICENSE,sha256=iUaO1XZyB9P3Tmog0OILuTisP6vXGe3QKz-4yRTxOFk,1069
|
|
44
|
+
python_general_be_lib-0.12.2.dist-info/METADATA,sha256=y3oeQsu0X1IYTfpOIE4pblBBEvMr9KQu8psiK4TIeiQ,3995
|
|
45
|
+
python_general_be_lib-0.12.2.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
46
|
+
python_general_be_lib-0.12.2.dist-info/top_level.txt,sha256=tTZePW8_CNUqSgKFd2SEH72ZbnhS0OYjRsgcv0ikSFY,8
|
|
47
|
+
python_general_be_lib-0.12.2.dist-info/RECORD,,
|
|
File without changes
|
{python_general_be_lib-0.12.1.dist-info → python_general_be_lib-0.12.2.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{python_general_be_lib-0.12.1.dist-info → python_general_be_lib-0.12.2.dist-info}/top_level.txt
RENAMED
|
File without changes
|