jararaca 0.2.37a12__py3-none-any.whl → 0.4.0a5__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.
- README.md +121 -0
- jararaca/__init__.py +267 -15
- jararaca/__main__.py +4 -0
- jararaca/broker_backend/__init__.py +106 -0
- jararaca/broker_backend/mapper.py +25 -0
- jararaca/broker_backend/redis_broker_backend.py +168 -0
- jararaca/cli.py +840 -103
- jararaca/common/__init__.py +3 -0
- jararaca/core/__init__.py +3 -0
- jararaca/core/providers.py +4 -0
- jararaca/core/uow.py +55 -16
- jararaca/di.py +4 -0
- jararaca/files/entity.py.mako +4 -0
- jararaca/lifecycle.py +6 -2
- jararaca/messagebus/__init__.py +5 -1
- jararaca/messagebus/bus_message_controller.py +4 -0
- jararaca/messagebus/consumers/__init__.py +3 -0
- jararaca/messagebus/decorators.py +90 -85
- jararaca/messagebus/implicit_headers.py +49 -0
- jararaca/messagebus/interceptors/__init__.py +3 -0
- jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +95 -37
- jararaca/messagebus/interceptors/publisher_interceptor.py +42 -0
- jararaca/messagebus/message.py +31 -0
- jararaca/messagebus/publisher.py +47 -4
- jararaca/messagebus/worker.py +1615 -135
- jararaca/microservice.py +248 -36
- jararaca/observability/constants.py +7 -0
- jararaca/observability/decorators.py +177 -16
- jararaca/observability/fastapi_exception_handler.py +37 -0
- jararaca/observability/hooks.py +109 -0
- jararaca/observability/interceptor.py +8 -2
- jararaca/observability/providers/__init__.py +3 -0
- jararaca/observability/providers/otel.py +213 -18
- jararaca/persistence/base.py +40 -3
- jararaca/persistence/exports.py +4 -0
- jararaca/persistence/interceptors/__init__.py +3 -0
- jararaca/persistence/interceptors/aiosqa_interceptor.py +187 -23
- jararaca/persistence/interceptors/constants.py +5 -0
- jararaca/persistence/interceptors/decorators.py +50 -0
- jararaca/persistence/session.py +3 -0
- jararaca/persistence/sort_filter.py +4 -0
- jararaca/persistence/utilities.py +74 -32
- jararaca/presentation/__init__.py +3 -0
- jararaca/presentation/decorators.py +170 -82
- jararaca/presentation/exceptions.py +23 -0
- jararaca/presentation/hooks.py +4 -0
- jararaca/presentation/http_microservice.py +4 -0
- jararaca/presentation/server.py +120 -41
- jararaca/presentation/websocket/__init__.py +3 -0
- jararaca/presentation/websocket/base_types.py +4 -0
- jararaca/presentation/websocket/context.py +34 -4
- jararaca/presentation/websocket/decorators.py +8 -41
- jararaca/presentation/websocket/redis.py +280 -53
- jararaca/presentation/websocket/types.py +6 -2
- jararaca/presentation/websocket/websocket_interceptor.py +74 -23
- jararaca/reflect/__init__.py +3 -0
- jararaca/reflect/controller_inspect.py +81 -0
- jararaca/reflect/decorators.py +238 -0
- jararaca/reflect/metadata.py +76 -0
- jararaca/rpc/__init__.py +3 -0
- jararaca/rpc/http/__init__.py +101 -0
- jararaca/rpc/http/backends/__init__.py +14 -0
- jararaca/rpc/http/backends/httpx.py +43 -9
- jararaca/rpc/http/backends/otel.py +4 -0
- jararaca/rpc/http/decorators.py +378 -113
- jararaca/rpc/http/httpx.py +3 -0
- jararaca/scheduler/__init__.py +3 -0
- jararaca/scheduler/beat_worker.py +758 -0
- jararaca/scheduler/decorators.py +89 -28
- jararaca/scheduler/types.py +11 -0
- jararaca/tools/app_config/__init__.py +3 -0
- jararaca/tools/app_config/decorators.py +7 -19
- jararaca/tools/app_config/interceptor.py +10 -4
- jararaca/tools/typescript/__init__.py +3 -0
- jararaca/tools/typescript/decorators.py +120 -0
- jararaca/tools/typescript/interface_parser.py +1126 -189
- jararaca/utils/__init__.py +3 -0
- jararaca/utils/rabbitmq_utils.py +372 -0
- jararaca/utils/retry.py +148 -0
- jararaca-0.4.0a5.dist-info/LICENSE +674 -0
- jararaca-0.4.0a5.dist-info/LICENSES/GPL-3.0-or-later.txt +232 -0
- {jararaca-0.2.37a12.dist-info → jararaca-0.4.0a5.dist-info}/METADATA +14 -7
- jararaca-0.4.0a5.dist-info/RECORD +88 -0
- {jararaca-0.2.37a12.dist-info → jararaca-0.4.0a5.dist-info}/WHEEL +1 -1
- pyproject.toml +131 -0
- jararaca/messagebus/types.py +0 -30
- jararaca/scheduler/scheduler.py +0 -154
- jararaca/tools/metadata.py +0 -47
- jararaca-0.2.37a12.dist-info/RECORD +0 -63
- /jararaca-0.2.37a12.dist-info/LICENSE → /LICENSE +0 -0
- {jararaca-0.2.37a12.dist-info → jararaca-0.4.0a5.dist-info}/entry_points.txt +0 -0
|
@@ -1,23 +1,97 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
1
5
|
from contextlib import asynccontextmanager, contextmanager, suppress
|
|
2
6
|
from contextvars import ContextVar
|
|
3
7
|
from dataclasses import dataclass
|
|
4
|
-
from typing import Any, AsyncGenerator, Generator
|
|
5
|
-
|
|
6
|
-
from sqlalchemy.ext.asyncio import
|
|
8
|
+
from typing import Any, AsyncGenerator, Generator, Protocol
|
|
9
|
+
|
|
10
|
+
from sqlalchemy.ext.asyncio import (
|
|
11
|
+
AsyncSession,
|
|
12
|
+
AsyncSessionTransaction,
|
|
13
|
+
async_sessionmaker,
|
|
14
|
+
create_async_engine,
|
|
15
|
+
)
|
|
7
16
|
from sqlalchemy.ext.asyncio.engine import AsyncEngine
|
|
8
17
|
|
|
9
|
-
from jararaca.microservice import
|
|
18
|
+
from jararaca.microservice import AppInterceptor, AppTransactionContext
|
|
19
|
+
from jararaca.persistence.interceptors.constants import DEFAULT_CONNECTION_NAME
|
|
20
|
+
from jararaca.persistence.interceptors.decorators import (
|
|
21
|
+
INJECT_PERSISTENCE_SESSION_METADATA_TEMPLATE,
|
|
22
|
+
)
|
|
23
|
+
from jararaca.reflect.metadata import get_metadata_value
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SessionManager(Protocol):
|
|
27
|
+
def spawn_session(self, connection_name: str | None = None) -> AsyncSession: ...
|
|
10
28
|
|
|
11
|
-
|
|
29
|
+
|
|
30
|
+
ctx_session_manager: ContextVar[SessionManager | None] = ContextVar(
|
|
31
|
+
"ctx_session_manager", default=None
|
|
32
|
+
)
|
|
12
33
|
|
|
13
34
|
|
|
14
35
|
@contextmanager
|
|
15
|
-
def
|
|
16
|
-
|
|
36
|
+
def providing_session_manager(
|
|
37
|
+
session_manager: SessionManager,
|
|
17
38
|
) -> Generator[None, Any, None]:
|
|
39
|
+
"""
|
|
40
|
+
Context manager to provide a session manager for the current context.
|
|
41
|
+
"""
|
|
42
|
+
token = ctx_session_manager.set(session_manager)
|
|
43
|
+
try:
|
|
44
|
+
yield
|
|
45
|
+
finally:
|
|
46
|
+
with suppress(ValueError):
|
|
47
|
+
ctx_session_manager.reset(token)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def use_session_manager() -> SessionManager:
|
|
51
|
+
"""
|
|
52
|
+
Retrieve the current session manager from the context variable.
|
|
53
|
+
Raises ValueError if no session manager is set.
|
|
54
|
+
"""
|
|
55
|
+
session_manager = ctx_session_manager.get()
|
|
56
|
+
if session_manager is None:
|
|
57
|
+
raise ValueError("No session manager set in the context.")
|
|
58
|
+
return session_manager
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
ctx_default_connection_name: ContextVar[str] = ContextVar(
|
|
62
|
+
"ctx_default_connection_name", default=DEFAULT_CONNECTION_NAME
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def ensure_name(name: str | None) -> str:
|
|
67
|
+
return ctx_default_connection_name.get()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class PersistenceCtx:
|
|
72
|
+
session: AsyncSession
|
|
73
|
+
tx: AsyncSessionTransaction
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
ctx_session_map = ContextVar[dict[str, PersistenceCtx]]("ctx_session_map", default={})
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@contextmanager
|
|
80
|
+
def providing_session(
|
|
81
|
+
session: AsyncSession,
|
|
82
|
+
tx: AsyncSessionTransaction,
|
|
83
|
+
connection_name: str | None = None,
|
|
84
|
+
) -> Generator[None, Any, None]:
|
|
85
|
+
"""
|
|
86
|
+
Context manager to provide a session and transaction for a specific connection name.
|
|
87
|
+
If no connection name is provided, it uses the default connection name from the context variable.
|
|
88
|
+
"""
|
|
89
|
+
connection_name = ensure_name(connection_name)
|
|
18
90
|
current_map = ctx_session_map.get({})
|
|
19
91
|
|
|
20
|
-
token = ctx_session_map.set(
|
|
92
|
+
token = ctx_session_map.set(
|
|
93
|
+
{**current_map, connection_name: PersistenceCtx(session, tx)}
|
|
94
|
+
)
|
|
21
95
|
|
|
22
96
|
try:
|
|
23
97
|
yield
|
|
@@ -26,21 +100,86 @@ def provide_session(
|
|
|
26
100
|
ctx_session_map.reset(token)
|
|
27
101
|
|
|
28
102
|
|
|
29
|
-
|
|
103
|
+
provide_session = providing_session
|
|
104
|
+
"""
|
|
105
|
+
Alias for `providing_session` to maintain backward compatibility.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@asynccontextmanager
|
|
110
|
+
async def providing_new_session(
|
|
111
|
+
connection_name: str | None = None,
|
|
112
|
+
) -> AsyncGenerator[AsyncSession, None]:
|
|
113
|
+
|
|
114
|
+
session_manager = use_session_manager()
|
|
115
|
+
current_session = session_manager.spawn_session(connection_name)
|
|
116
|
+
|
|
117
|
+
async with AsyncSession(
|
|
118
|
+
current_session.bind,
|
|
119
|
+
) as new_session, new_session.begin() as new_tx:
|
|
120
|
+
with providing_session(new_session, new_tx, connection_name):
|
|
121
|
+
try:
|
|
122
|
+
yield new_session
|
|
123
|
+
if new_tx.is_active:
|
|
124
|
+
await new_tx.commit()
|
|
125
|
+
except Exception:
|
|
126
|
+
if new_tx.is_active:
|
|
127
|
+
await new_tx.rollback()
|
|
128
|
+
raise
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def use_session(connection_name: str | None = None) -> AsyncSession:
|
|
132
|
+
connection_name = ensure_name(connection_name)
|
|
30
133
|
current_map = ctx_session_map.get({})
|
|
31
134
|
if connection_name not in current_map:
|
|
32
|
-
raise ValueError(
|
|
135
|
+
raise ValueError(
|
|
136
|
+
f'Session not found for connection "{connection_name}" in context. Check if your interceptor is correctly set up.'
|
|
137
|
+
)
|
|
33
138
|
|
|
34
|
-
return current_map[connection_name]
|
|
139
|
+
return current_map[connection_name].session
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@contextmanager
|
|
143
|
+
def providing_transaction(
|
|
144
|
+
tx: AsyncSessionTransaction,
|
|
145
|
+
connection_name: str | None = None,
|
|
146
|
+
) -> Generator[None, Any, None]:
|
|
147
|
+
connection_name = ensure_name(connection_name)
|
|
148
|
+
|
|
149
|
+
current_map = ctx_session_map.get({})
|
|
150
|
+
|
|
151
|
+
if connection_name not in current_map:
|
|
152
|
+
raise ValueError(f"No session found for connection {connection_name}")
|
|
153
|
+
|
|
154
|
+
with providing_session(current_map[connection_name].session, tx, connection_name):
|
|
155
|
+
yield
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def use_transaction(connection_name: str | None = None) -> AsyncSessionTransaction:
|
|
159
|
+
current_map = ctx_session_map.get({})
|
|
160
|
+
if connection_name not in current_map:
|
|
161
|
+
raise ValueError(f"Transaction not found for connection {connection_name}")
|
|
162
|
+
|
|
163
|
+
return current_map[connection_name].tx
|
|
35
164
|
|
|
36
165
|
|
|
37
|
-
@dataclass
|
|
38
166
|
class AIOSQAConfig:
|
|
39
|
-
connection_name: str
|
|
40
167
|
url: str | AsyncEngine
|
|
168
|
+
connection_name: str
|
|
169
|
+
inject_default: bool
|
|
170
|
+
|
|
171
|
+
def __init__(
|
|
172
|
+
self,
|
|
173
|
+
url: str | AsyncEngine,
|
|
174
|
+
connection_name: str = DEFAULT_CONNECTION_NAME,
|
|
175
|
+
inject_default: bool = True,
|
|
176
|
+
):
|
|
177
|
+
self.url = url
|
|
178
|
+
self.connection_name = connection_name
|
|
179
|
+
self.inject_default = inject_default
|
|
41
180
|
|
|
42
181
|
|
|
43
|
-
class AIOSqlAlchemySessionInterceptor(AppInterceptor):
|
|
182
|
+
class AIOSqlAlchemySessionInterceptor(AppInterceptor, SessionManager):
|
|
44
183
|
|
|
45
184
|
def __init__(self, config: AIOSQAConfig):
|
|
46
185
|
self.config = config
|
|
@@ -53,12 +192,37 @@ class AIOSqlAlchemySessionInterceptor(AppInterceptor):
|
|
|
53
192
|
self.sessionmaker = async_sessionmaker(self.engine)
|
|
54
193
|
|
|
55
194
|
@asynccontextmanager
|
|
56
|
-
async def intercept(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
195
|
+
async def intercept(
|
|
196
|
+
self, app_context: AppTransactionContext
|
|
197
|
+
) -> AsyncGenerator[None, None]:
|
|
198
|
+
|
|
199
|
+
with providing_session_manager(self):
|
|
200
|
+
uses_connection_metadata = get_metadata_value(
|
|
201
|
+
INJECT_PERSISTENCE_SESSION_METADATA_TEMPLATE.format(
|
|
202
|
+
connection_name=self.config.connection_name
|
|
203
|
+
),
|
|
204
|
+
self.config.inject_default,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if not uses_connection_metadata:
|
|
208
|
+
yield
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
async with self.sessionmaker() as session, session.begin() as tx:
|
|
212
|
+
token = ctx_default_connection_name.set(self.config.connection_name)
|
|
213
|
+
with providing_session(session, tx, self.config.connection_name):
|
|
214
|
+
try:
|
|
215
|
+
yield
|
|
216
|
+
if tx.is_active:
|
|
217
|
+
await tx.commit()
|
|
218
|
+
except Exception as e:
|
|
219
|
+
await tx.rollback()
|
|
220
|
+
raise e
|
|
221
|
+
finally:
|
|
222
|
+
with suppress(ValueError):
|
|
223
|
+
ctx_default_connection_name.reset(token)
|
|
224
|
+
|
|
225
|
+
def spawn_session(self, connection_name: str | None = None) -> AsyncSession:
|
|
226
|
+
connection_name = ensure_name(connection_name)
|
|
227
|
+
session = self.sessionmaker()
|
|
228
|
+
return session
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from jararaca.persistence.interceptors.constants import DEFAULT_CONNECTION_NAME
|
|
7
|
+
from jararaca.reflect.metadata import SetMetadata
|
|
8
|
+
|
|
9
|
+
INJECT_PERSISTENCE_SESSION_METADATA_TEMPLATE = (
|
|
10
|
+
"inject_persistence_template_{connection_name}"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def set_use_persistence_session(
|
|
15
|
+
inject: bool, connection_name: str = DEFAULT_CONNECTION_NAME
|
|
16
|
+
) -> SetMetadata:
|
|
17
|
+
"""
|
|
18
|
+
Set whether to inject the connection metadata for the given connection name.
|
|
19
|
+
This is useful when you want to control whether the connection metadata
|
|
20
|
+
should be injected into the context or not.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
return SetMetadata(
|
|
24
|
+
INJECT_PERSISTENCE_SESSION_METADATA_TEMPLATE.format(
|
|
25
|
+
connection_name=connection_name
|
|
26
|
+
),
|
|
27
|
+
inject,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def uses_persistence_session(
|
|
32
|
+
connection_name: str = DEFAULT_CONNECTION_NAME,
|
|
33
|
+
) -> SetMetadata:
|
|
34
|
+
"""
|
|
35
|
+
Use connection metadata for the given connection name.
|
|
36
|
+
This is useful when you want to inject the connection metadata into the context,
|
|
37
|
+
for example, when you are using a specific connection for a specific operation.
|
|
38
|
+
"""
|
|
39
|
+
return set_use_persistence_session(True, connection_name=connection_name)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def skip_persistence_session(
|
|
43
|
+
connection_name: str = DEFAULT_CONNECTION_NAME,
|
|
44
|
+
) -> SetMetadata:
|
|
45
|
+
"""
|
|
46
|
+
Decorator to skip using connection metadata for the given connection name.
|
|
47
|
+
This is useful when you want to ensure that the connection metadata is not injected
|
|
48
|
+
into the context, for example, when you are using a different connection for a specific operation.
|
|
49
|
+
"""
|
|
50
|
+
return set_use_persistence_session(False, connection_name=connection_name)
|
jararaca/persistence/session.py
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
1
5
|
import asyncio
|
|
2
6
|
import logging
|
|
7
|
+
import math
|
|
3
8
|
from datetime import UTC, date, datetime
|
|
4
9
|
from functools import reduce
|
|
5
10
|
from typing import (
|
|
@@ -29,6 +34,7 @@ from jararaca.persistence.base import (
|
|
|
29
34
|
BaseEntity,
|
|
30
35
|
recursive_get_dict,
|
|
31
36
|
)
|
|
37
|
+
from jararaca.persistence.interceptors.aiosqa_interceptor import use_session
|
|
32
38
|
from jararaca.persistence.sort_filter import (
|
|
33
39
|
FilterModel,
|
|
34
40
|
FilterRuleApplier,
|
|
@@ -61,7 +67,7 @@ class DatedEntity(BaseEntity):
|
|
|
61
67
|
DateTime(timezone=True), nullable=False, default=nowutc
|
|
62
68
|
)
|
|
63
69
|
updated_at: Mapped[datetime] = mapped_column(
|
|
64
|
-
DateTime(timezone=True), nullable=False, default=nowutc
|
|
70
|
+
DateTime(timezone=True), nullable=False, default=nowutc, onupdate=nowutc
|
|
65
71
|
)
|
|
66
72
|
|
|
67
73
|
|
|
@@ -213,7 +219,7 @@ class CRUDOperations(Generic[IDENTIFIABLE_T]):
|
|
|
213
219
|
# region PaginatedFilter
|
|
214
220
|
class PaginatedFilter(BaseModel):
|
|
215
221
|
page: Annotated[int, Field(gt=-1)] = 0
|
|
216
|
-
page_size: int = 10
|
|
222
|
+
page_size: Annotated[int, Field(gt=0)] = 10
|
|
217
223
|
sort_models: list[SortModel] = []
|
|
218
224
|
filter_models: list[FilterModel] = []
|
|
219
225
|
|
|
@@ -283,11 +289,13 @@ class QueryOperations(Generic[QUERY_FILTER_T, QUERY_ENTITY_T]):
|
|
|
283
289
|
def __init__(
|
|
284
290
|
self,
|
|
285
291
|
entity_type: Type[QUERY_ENTITY_T],
|
|
286
|
-
session_provider: Callable[[], AsyncSession],
|
|
287
|
-
filters_functions: list[QueryInjector],
|
|
292
|
+
session_provider: Callable[[], AsyncSession] = use_session,
|
|
293
|
+
filters_functions: list[QueryInjector] = [],
|
|
294
|
+
*,
|
|
288
295
|
unique: bool = False,
|
|
289
296
|
sort_rule_applier: SortRuleApplier | None = None,
|
|
290
297
|
filter_rule_applier: FilterRuleApplier | None = None,
|
|
298
|
+
base_statement: Select[Tuple[QUERY_ENTITY_T]] | None = None,
|
|
291
299
|
) -> None:
|
|
292
300
|
self.entity_type: type[QUERY_ENTITY_T] = entity_type
|
|
293
301
|
self.session_provider = session_provider
|
|
@@ -295,6 +303,7 @@ class QueryOperations(Generic[QUERY_FILTER_T, QUERY_ENTITY_T]):
|
|
|
295
303
|
self.unique = unique
|
|
296
304
|
self.sort_rule_applier = sort_rule_applier
|
|
297
305
|
self.filter_rule_applier = filter_rule_applier
|
|
306
|
+
self.base_statement = base_statement
|
|
298
307
|
|
|
299
308
|
@property
|
|
300
309
|
def session(self) -> AsyncSession:
|
|
@@ -303,55 +312,88 @@ class QueryOperations(Generic[QUERY_FILTER_T, QUERY_ENTITY_T]):
|
|
|
303
312
|
async def query(
|
|
304
313
|
self,
|
|
305
314
|
filter: QUERY_FILTER_T,
|
|
315
|
+
*,
|
|
306
316
|
interceptors: list[
|
|
307
317
|
Callable[[Select[Tuple[QUERY_ENTITY_T]]], Select[Tuple[QUERY_ENTITY_T]]]
|
|
308
318
|
] = [],
|
|
319
|
+
base_statement: (
|
|
320
|
+
Callable[[Select[Tuple[QUERY_ENTITY_T]]], Select[Tuple[QUERY_ENTITY_T]]]
|
|
321
|
+
| Select[Tuple[QUERY_ENTITY_T]]
|
|
322
|
+
| None
|
|
323
|
+
) = None,
|
|
309
324
|
) -> "Paginated[QUERY_ENTITY_T]":
|
|
325
|
+
"""
|
|
326
|
+
Executes a query with the provided filter and interceptors.
|
|
327
|
+
Args:
|
|
328
|
+
filter: The filter to apply to the query.
|
|
329
|
+
interceptors: A list of functions that can modify the query before execution.
|
|
330
|
+
Returns:
|
|
331
|
+
Paginated[QUERY_ENTITY_T]: A paginated result containing the items and metadata.
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
initial_statement = self.base_statement or select(self.entity_type)
|
|
335
|
+
|
|
336
|
+
if base_statement and callable(base_statement):
|
|
337
|
+
initial_statement = base_statement(initial_statement)
|
|
338
|
+
elif base_statement and isinstance(base_statement, Select):
|
|
339
|
+
initial_statement = base_statement
|
|
340
|
+
|
|
341
|
+
tier_one_filtered_query = self.generate_filtered_query(
|
|
342
|
+
filter, initial_statement
|
|
343
|
+
)
|
|
310
344
|
|
|
311
|
-
|
|
345
|
+
tier_two_filtered_query = reduce(
|
|
312
346
|
lambda query, interceptor: interceptor(query),
|
|
313
347
|
interceptors,
|
|
314
|
-
|
|
348
|
+
tier_one_filtered_query,
|
|
315
349
|
)
|
|
316
350
|
|
|
317
351
|
if self.sort_rule_applier:
|
|
318
|
-
|
|
319
|
-
|
|
352
|
+
tier_two_filtered_query = (
|
|
353
|
+
self.sort_rule_applier.create_query_for_sorting_list(
|
|
354
|
+
tier_two_filtered_query, filter.sort_models
|
|
355
|
+
)
|
|
320
356
|
)
|
|
321
357
|
|
|
322
358
|
if self.filter_rule_applier:
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
unpaginated_total = (
|
|
328
|
-
await self.session.execute(
|
|
329
|
-
select(func.count()).select_from(query.subquery())
|
|
359
|
+
tier_two_filtered_query = (
|
|
360
|
+
self.filter_rule_applier.create_query_for_filter_list(
|
|
361
|
+
tier_two_filtered_query, filter.filter_models
|
|
362
|
+
)
|
|
330
363
|
)
|
|
331
|
-
).scalar_one()
|
|
332
364
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
365
|
+
paginated_query = tier_two_filtered_query.add_columns(
|
|
366
|
+
func.count().over().label("total_count")
|
|
367
|
+
)
|
|
368
|
+
paginated_query = paginated_query.limit(filter.page_size).offset(
|
|
336
369
|
(filter.page) * filter.page_size
|
|
337
370
|
)
|
|
338
371
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
372
|
+
result = await self.session.execute(paginated_query)
|
|
373
|
+
result = self.judge_unique(result)
|
|
374
|
+
rows = result.all()
|
|
375
|
+
|
|
376
|
+
if rows:
|
|
377
|
+
unpaginated_total = rows[0].total_count
|
|
378
|
+
result_scalars = [row[0] for row in rows]
|
|
379
|
+
else:
|
|
380
|
+
result_scalars = []
|
|
381
|
+
if filter.page == 0:
|
|
382
|
+
unpaginated_total = 0
|
|
383
|
+
else:
|
|
384
|
+
unpaginated_total = (
|
|
385
|
+
await self.session.execute(
|
|
386
|
+
select(func.count()).select_from(
|
|
387
|
+
tier_two_filtered_query.subquery()
|
|
388
|
+
)
|
|
389
|
+
)
|
|
390
|
+
).scalar_one()
|
|
344
391
|
|
|
345
392
|
return Paginated(
|
|
346
|
-
items=
|
|
347
|
-
|
|
348
|
-
for e in self.judge_unique(
|
|
349
|
-
await self.session.execute(paginated_query)
|
|
350
|
-
).scalars()
|
|
351
|
-
],
|
|
352
|
-
total=filtered_total,
|
|
393
|
+
items=result_scalars,
|
|
394
|
+
total=len(result_scalars),
|
|
353
395
|
unpaginated_total=unpaginated_total,
|
|
354
|
-
total_pages=
|
|
396
|
+
total_pages=math.ceil(unpaginated_total / filter.page_size),
|
|
355
397
|
)
|
|
356
398
|
|
|
357
399
|
def judge_unique(
|