jararaca 0.3.11a16__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 +184 -12
- jararaca/__main__.py +4 -0
- jararaca/broker_backend/__init__.py +4 -0
- jararaca/broker_backend/mapper.py +4 -0
- jararaca/broker_backend/redis_broker_backend.py +9 -3
- jararaca/cli.py +272 -47
- jararaca/common/__init__.py +3 -0
- jararaca/core/__init__.py +3 -0
- jararaca/core/providers.py +4 -0
- jararaca/core/uow.py +41 -7
- jararaca/di.py +4 -0
- jararaca/files/entity.py.mako +4 -0
- jararaca/lifecycle.py +6 -2
- jararaca/messagebus/__init__.py +4 -0
- jararaca/messagebus/bus_message_controller.py +4 -0
- jararaca/messagebus/consumers/__init__.py +3 -0
- jararaca/messagebus/decorators.py +33 -67
- jararaca/messagebus/implicit_headers.py +49 -0
- jararaca/messagebus/interceptors/__init__.py +3 -0
- jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +13 -4
- jararaca/messagebus/interceptors/publisher_interceptor.py +4 -0
- jararaca/messagebus/message.py +4 -0
- jararaca/messagebus/publisher.py +6 -0
- jararaca/messagebus/worker.py +850 -383
- jararaca/microservice.py +110 -1
- jararaca/observability/constants.py +7 -0
- jararaca/observability/decorators.py +170 -13
- jararaca/observability/fastapi_exception_handler.py +37 -0
- jararaca/observability/hooks.py +109 -0
- jararaca/observability/interceptor.py +4 -0
- jararaca/observability/providers/__init__.py +3 -0
- jararaca/observability/providers/otel.py +202 -11
- jararaca/persistence/base.py +38 -2
- jararaca/persistence/exports.py +4 -0
- jararaca/persistence/interceptors/__init__.py +3 -0
- jararaca/persistence/interceptors/aiosqa_interceptor.py +86 -73
- 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 +50 -20
- jararaca/presentation/__init__.py +3 -0
- jararaca/presentation/decorators.py +88 -86
- jararaca/presentation/exceptions.py +23 -0
- jararaca/presentation/hooks.py +4 -0
- jararaca/presentation/http_microservice.py +4 -0
- jararaca/presentation/server.py +97 -45
- jararaca/presentation/websocket/__init__.py +3 -0
- jararaca/presentation/websocket/base_types.py +4 -0
- jararaca/presentation/websocket/context.py +4 -0
- jararaca/presentation/websocket/decorators.py +8 -41
- jararaca/presentation/websocket/redis.py +280 -53
- jararaca/presentation/websocket/types.py +4 -0
- jararaca/presentation/websocket/websocket_interceptor.py +46 -19
- jararaca/reflect/__init__.py +3 -0
- jararaca/reflect/controller_inspect.py +16 -10
- jararaca/reflect/decorators.py +238 -0
- jararaca/reflect/metadata.py +34 -25
- 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 +521 -105
- jararaca/scheduler/decorators.py +15 -22
- jararaca/scheduler/types.py +4 -0
- jararaca/tools/app_config/__init__.py +3 -0
- jararaca/tools/app_config/decorators.py +7 -19
- jararaca/tools/app_config/interceptor.py +6 -2
- jararaca/tools/typescript/__init__.py +3 -0
- jararaca/tools/typescript/decorators.py +120 -0
- jararaca/tools/typescript/interface_parser.py +1074 -173
- jararaca/utils/__init__.py +3 -0
- jararaca/utils/rabbitmq_utils.py +65 -39
- jararaca/utils/retry.py +10 -3
- 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.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/METADATA +11 -7
- jararaca-0.4.0a5.dist-info/RECORD +88 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/WHEEL +1 -1
- pyproject.toml +131 -0
- jararaca-0.3.11a16.dist-info/RECORD +0 -74
- /jararaca-0.3.11a16.dist-info/LICENSE → /LICENSE +0 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/entry_points.txt +0 -0
|
@@ -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
|
|
|
@@ -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,9 +312,15 @@ 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]":
|
|
310
325
|
"""
|
|
311
326
|
Executes a query with the provided filter and interceptors.
|
|
@@ -316,8 +331,15 @@ class QueryOperations(Generic[QUERY_FILTER_T, QUERY_ENTITY_T]):
|
|
|
316
331
|
Paginated[QUERY_ENTITY_T]: A paginated result containing the items and metadata.
|
|
317
332
|
"""
|
|
318
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
|
+
|
|
319
341
|
tier_one_filtered_query = self.generate_filtered_query(
|
|
320
|
-
filter,
|
|
342
|
+
filter, initial_statement
|
|
321
343
|
)
|
|
322
344
|
|
|
323
345
|
tier_two_filtered_query = reduce(
|
|
@@ -340,30 +362,38 @@ class QueryOperations(Generic[QUERY_FILTER_T, QUERY_ENTITY_T]):
|
|
|
340
362
|
)
|
|
341
363
|
)
|
|
342
364
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
).scalar_one()
|
|
348
|
-
|
|
349
|
-
paginated_query = tier_two_filtered_query.limit(filter.page_size).offset(
|
|
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(
|
|
350
369
|
(filter.page) * filter.page_size
|
|
351
370
|
)
|
|
352
371
|
|
|
353
|
-
paginated_total = (
|
|
354
|
-
await self.session.execute(
|
|
355
|
-
select(func.count()).select_from(paginated_query.subquery())
|
|
356
|
-
)
|
|
357
|
-
).scalar_one()
|
|
358
|
-
|
|
359
372
|
result = await self.session.execute(paginated_query)
|
|
360
|
-
|
|
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()
|
|
361
391
|
|
|
362
392
|
return Paginated(
|
|
363
393
|
items=result_scalars,
|
|
364
|
-
total=
|
|
394
|
+
total=len(result_scalars),
|
|
365
395
|
unpaginated_total=unpaginated_total,
|
|
366
|
-
total_pages=
|
|
396
|
+
total_pages=math.ceil(unpaginated_total / filter.page_size),
|
|
367
397
|
)
|
|
368
398
|
|
|
369
399
|
def judge_unique(
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
1
6
|
from contextlib import contextmanager
|
|
2
7
|
from contextvars import ContextVar
|
|
3
8
|
from functools import wraps
|
|
4
|
-
from typing import Any, Awaitable, Callable, Literal, Protocol, TypeVar
|
|
9
|
+
from typing import Any, Awaitable, Callable, Literal, Mapping, Protocol, TypeVar
|
|
5
10
|
|
|
6
11
|
from fastapi import APIRouter
|
|
7
12
|
from fastapi import Depends as DependsF
|
|
@@ -15,12 +20,13 @@ from jararaca.reflect.controller_inspect import (
|
|
|
15
20
|
ControllerMemberReflect,
|
|
16
21
|
inspect_controller,
|
|
17
22
|
)
|
|
23
|
+
from jararaca.reflect.decorators import DECORATED_T, StackableDecorator
|
|
18
24
|
|
|
25
|
+
DECORATED_TYPE = TypeVar("DECORATED_TYPE", bound=Any)
|
|
19
26
|
DECORATED_FUNC = TypeVar("DECORATED_FUNC", bound=Callable[..., Any])
|
|
20
|
-
DECORATED_CLASS = TypeVar("DECORATED_CLASS", bound=Any)
|
|
21
27
|
|
|
22
28
|
|
|
23
|
-
ControllerOptions =
|
|
29
|
+
ControllerOptions = Mapping[str, Any]
|
|
24
30
|
|
|
25
31
|
|
|
26
32
|
class RouterFactory(Protocol):
|
|
@@ -28,20 +34,25 @@ class RouterFactory(Protocol):
|
|
|
28
34
|
def __call__(self, lifecycle: AppLifecycle, instance: Any) -> APIRouter: ...
|
|
29
35
|
|
|
30
36
|
|
|
31
|
-
class RestController:
|
|
37
|
+
class RestController(StackableDecorator):
|
|
32
38
|
REST_CONTROLLER_ATTR = "__rest_controller__"
|
|
33
39
|
|
|
34
40
|
def __init__(
|
|
35
41
|
self,
|
|
36
42
|
path: str = "",
|
|
43
|
+
*,
|
|
37
44
|
options: ControllerOptions | None = None,
|
|
38
45
|
middlewares: list[type[HttpMiddleware]] = [],
|
|
39
46
|
router_factory: RouterFactory | None = None,
|
|
47
|
+
class_inherits_decorators: bool = True,
|
|
48
|
+
methods_inherit_decorators: bool = True,
|
|
40
49
|
) -> None:
|
|
41
50
|
self.path = path
|
|
42
51
|
self.options = options
|
|
43
52
|
self.router_factory = router_factory
|
|
44
53
|
self.middlewares = middlewares
|
|
54
|
+
self.class_inherits_decorators = class_inherits_decorators
|
|
55
|
+
self.methods_inherit_decorators = methods_inherit_decorators
|
|
45
56
|
|
|
46
57
|
def get_router_factory(
|
|
47
58
|
self,
|
|
@@ -50,12 +61,15 @@ class RestController:
|
|
|
50
61
|
raise Exception("Router factory is not set")
|
|
51
62
|
return self.router_factory
|
|
52
63
|
|
|
53
|
-
def
|
|
64
|
+
def post_decorated(self, subject: DECORATED_T) -> None:
|
|
54
65
|
|
|
55
66
|
def router_factory(
|
|
56
67
|
lifecycle: AppLifecycle,
|
|
57
|
-
instance:
|
|
68
|
+
instance: DECORATED_T,
|
|
58
69
|
) -> APIRouter:
|
|
70
|
+
assert inspect.isclass(
|
|
71
|
+
subject
|
|
72
|
+
), "RestController can only be applied to classes"
|
|
59
73
|
dependencies: list[Depends] = []
|
|
60
74
|
|
|
61
75
|
for self_middleware_type in self.middlewares:
|
|
@@ -64,13 +78,19 @@ class RestController:
|
|
|
64
78
|
)
|
|
65
79
|
dependencies.append(Depends(middleware_instance.intercept))
|
|
66
80
|
|
|
67
|
-
|
|
81
|
+
class_middlewares = UseMiddleware.get_all_from_type(
|
|
82
|
+
subject, self.class_inherits_decorators
|
|
83
|
+
)
|
|
84
|
+
for middlewares_by_hook in class_middlewares:
|
|
68
85
|
middleware_instance = lifecycle.container.get_by_type(
|
|
69
86
|
middlewares_by_hook.middleware
|
|
70
87
|
)
|
|
71
88
|
dependencies.append(Depends(middleware_instance.intercept))
|
|
72
89
|
|
|
73
|
-
|
|
90
|
+
class_dependencies = UseDependency.get_all_from_type(
|
|
91
|
+
subject, self.class_inherits_decorators
|
|
92
|
+
)
|
|
93
|
+
for dependency in class_dependencies:
|
|
74
94
|
dependencies.append(DependsF(dependency.dependency))
|
|
75
95
|
|
|
76
96
|
router = APIRouter(
|
|
@@ -79,15 +99,23 @@ class RestController:
|
|
|
79
99
|
**(self.options or {}),
|
|
80
100
|
)
|
|
81
101
|
|
|
82
|
-
controller, members = inspect_controller(
|
|
102
|
+
controller, members = inspect_controller(subject)
|
|
83
103
|
|
|
84
104
|
router_members = [
|
|
85
105
|
(name, mapping, member)
|
|
86
106
|
for name, member in members.items()
|
|
87
107
|
if (
|
|
88
|
-
mapping := (
|
|
89
|
-
|
|
90
|
-
|
|
108
|
+
mapping := HttpMapping.get_bound_from_method(
|
|
109
|
+
subject,
|
|
110
|
+
name,
|
|
111
|
+
self.methods_inherit_decorators,
|
|
112
|
+
last=True,
|
|
113
|
+
)
|
|
114
|
+
or WebSocketEndpoint.get_bound_from_method(
|
|
115
|
+
subject,
|
|
116
|
+
name,
|
|
117
|
+
self.methods_inherit_decorators,
|
|
118
|
+
last=True,
|
|
91
119
|
)
|
|
92
120
|
)
|
|
93
121
|
is not None
|
|
@@ -97,17 +125,22 @@ class RestController:
|
|
|
97
125
|
|
|
98
126
|
for name, mapping, member in router_members:
|
|
99
127
|
route_dependencies: list[Depends] = []
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
128
|
+
|
|
129
|
+
method_middlewares = UseMiddleware.get_all_from_method(
|
|
130
|
+
subject, name, self.methods_inherit_decorators
|
|
131
|
+
)
|
|
132
|
+
for middlewares_by_hook in method_middlewares:
|
|
103
133
|
middleware_instance = lifecycle.container.get_by_type(
|
|
104
134
|
middlewares_by_hook.middleware
|
|
105
135
|
)
|
|
106
136
|
route_dependencies.append(Depends(middleware_instance.intercept))
|
|
107
137
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
138
|
+
method_dependencies = UseDependency.get_all_from_method(
|
|
139
|
+
subject,
|
|
140
|
+
name,
|
|
141
|
+
self.methods_inherit_decorators,
|
|
142
|
+
)
|
|
143
|
+
for dependency in method_dependencies:
|
|
111
144
|
route_dependencies.append(DependsF(dependency.dependency))
|
|
112
145
|
|
|
113
146
|
instance_method = getattr(instance, name)
|
|
@@ -141,21 +174,6 @@ class RestController:
|
|
|
141
174
|
|
|
142
175
|
self.router_factory = router_factory
|
|
143
176
|
|
|
144
|
-
RestController.register(cls, self)
|
|
145
|
-
|
|
146
|
-
return cls
|
|
147
|
-
|
|
148
|
-
@staticmethod
|
|
149
|
-
def register(cls: type[DECORATED_CLASS], controller: "RestController") -> None:
|
|
150
|
-
setattr(cls, RestController.REST_CONTROLLER_ATTR, controller)
|
|
151
|
-
|
|
152
|
-
@staticmethod
|
|
153
|
-
def get_controller(cls: type[DECORATED_CLASS]) -> "RestController | None":
|
|
154
|
-
if not hasattr(cls, RestController.REST_CONTROLLER_ATTR):
|
|
155
|
-
return None
|
|
156
|
-
|
|
157
|
-
return cast(RestController, getattr(cls, RestController.REST_CONTROLLER_ATTR))
|
|
158
|
-
|
|
159
177
|
|
|
160
178
|
Options = dict[str, Any]
|
|
161
179
|
|
|
@@ -170,9 +188,8 @@ ResponseType = Literal[
|
|
|
170
188
|
]
|
|
171
189
|
|
|
172
190
|
|
|
173
|
-
class HttpMapping:
|
|
191
|
+
class HttpMapping(StackableDecorator):
|
|
174
192
|
|
|
175
|
-
HTTP_MAPPING_ATTR = "__http_mapping__"
|
|
176
193
|
ORDER_COUNTER = 0
|
|
177
194
|
|
|
178
195
|
def __init__(
|
|
@@ -190,24 +207,20 @@ class HttpMapping:
|
|
|
190
207
|
HttpMapping.ORDER_COUNTER += 1
|
|
191
208
|
self.order = HttpMapping.ORDER_COUNTER
|
|
192
209
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
HttpMapping
|
|
196
|
-
|
|
197
|
-
return func
|
|
210
|
+
@classmethod
|
|
211
|
+
def decorator_key(cls) -> "type[HttpMapping]":
|
|
212
|
+
return HttpMapping
|
|
198
213
|
|
|
199
|
-
|
|
200
|
-
def register(func: DECORATED_FUNC, mapping: "HttpMapping") -> None:
|
|
214
|
+
# def __call__(self, func: DECORATED_FUNC) -> DECORATED_FUNC:
|
|
201
215
|
|
|
202
|
-
|
|
216
|
+
# HttpMapping.register(func, self)
|
|
203
217
|
|
|
204
|
-
|
|
205
|
-
def get_http_mapping(func: DECORATED_FUNC) -> "HttpMapping | None":
|
|
218
|
+
# return func
|
|
206
219
|
|
|
207
|
-
|
|
208
|
-
|
|
220
|
+
# @staticmethod
|
|
221
|
+
# def register(func: DECORATED_FUNC, mapping: "HttpMapping") -> None:
|
|
209
222
|
|
|
210
|
-
|
|
223
|
+
# setattr(func, HttpMapping.HTTP_MAPPING_ATTR, mapping)
|
|
211
224
|
|
|
212
225
|
|
|
213
226
|
class Post(HttpMapping):
|
|
@@ -265,52 +278,46 @@ class Patch(HttpMapping):
|
|
|
265
278
|
super().__init__("PATCH", path, options, response_type)
|
|
266
279
|
|
|
267
280
|
|
|
268
|
-
class UseMiddleware:
|
|
269
|
-
|
|
270
|
-
__MIDDLEWARES_ATTR__ = "__middlewares__"
|
|
281
|
+
class UseMiddleware(StackableDecorator):
|
|
271
282
|
|
|
272
283
|
def __init__(self, middleware: type[HttpMiddleware]) -> None:
|
|
273
284
|
self.middleware = middleware
|
|
274
285
|
|
|
275
|
-
def __call__(self, subject: DECORATED_FUNC) -> DECORATED_FUNC:
|
|
276
|
-
|
|
277
|
-
UseMiddleware.register(subject, self)
|
|
278
|
-
|
|
279
|
-
return subject
|
|
280
|
-
|
|
281
|
-
@staticmethod
|
|
282
|
-
def register(subject: DECORATED_FUNC, middleware: "UseMiddleware") -> None:
|
|
283
|
-
middlewares = getattr(subject, UseMiddleware.__MIDDLEWARES_ATTR__, [])
|
|
284
|
-
middlewares.append(middleware)
|
|
285
|
-
setattr(subject, UseMiddleware.__MIDDLEWARES_ATTR__, middlewares)
|
|
286
|
-
|
|
287
|
-
@staticmethod
|
|
288
|
-
def get_middlewares(subject: DECORATED_FUNC) -> list["UseMiddleware"]:
|
|
289
|
-
return getattr(subject, UseMiddleware.__MIDDLEWARES_ATTR__, [])
|
|
290
|
-
|
|
291
286
|
|
|
292
|
-
class UseDependency:
|
|
293
|
-
|
|
294
|
-
__DEPENDENCY_ATTR__ = "__dependencies__"
|
|
287
|
+
class UseDependency(StackableDecorator):
|
|
295
288
|
|
|
296
289
|
def __init__(self, dependency: Any) -> None:
|
|
297
290
|
self.dependency = dependency
|
|
298
291
|
|
|
299
|
-
def __call__(self, subject: DECORATED_FUNC) -> DECORATED_FUNC:
|
|
300
292
|
|
|
301
|
-
|
|
293
|
+
def compose_route_decorators(
|
|
294
|
+
*decorators: UseMiddleware | UseDependency,
|
|
295
|
+
reverse: bool = False,
|
|
296
|
+
) -> Callable[[DECORATED_TYPE], DECORATED_TYPE]:
|
|
297
|
+
"""
|
|
298
|
+
Compose multiple route decorators (middlewares/dependencies) into a single decorator.
|
|
302
299
|
|
|
303
|
-
|
|
300
|
+
Args:
|
|
301
|
+
*decorators (UseMiddleware | UseDependency): The decorators to compose.
|
|
302
|
+
reverse (bool): Whether to apply the decorators in reverse order. Warning: This may affect the order of middleware execution.
|
|
304
303
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
dependencies = getattr(subject, UseDependency.__DEPENDENCY_ATTR__, [])
|
|
308
|
-
dependencies.append(dependency)
|
|
309
|
-
setattr(subject, UseDependency.__DEPENDENCY_ATTR__, dependencies)
|
|
304
|
+
Returns:
|
|
305
|
+
Callable[[DECORATED_TYPE], DECORATED_TYPE]: A single decorator that applies all the given decorators.
|
|
310
306
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
307
|
+
Example:
|
|
308
|
+
IsWorkspaceAdmin = compose_route_decorators(
|
|
309
|
+
UseMiddleware(AuthMiddleware),
|
|
310
|
+
UseMiddleware(IsWorkspaceScoped),
|
|
311
|
+
UseMiddleware(RequiresAdminRole),
|
|
312
|
+
)
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
def composed_decorator(func: DECORATED_TYPE) -> DECORATED_TYPE:
|
|
316
|
+
for decorator in reversed(decorators) if reverse else decorators:
|
|
317
|
+
func = decorator(func)
|
|
318
|
+
return func
|
|
319
|
+
|
|
320
|
+
return composed_decorator
|
|
314
321
|
|
|
315
322
|
|
|
316
323
|
def wraps_with_member_data(
|
|
@@ -330,11 +337,6 @@ def wraps_with_member_data(
|
|
|
330
337
|
|
|
331
338
|
return await func(*args, **kwargs)
|
|
332
339
|
|
|
333
|
-
# Copy metadata from the original function to the wrapper
|
|
334
|
-
# for attr in dir(func):
|
|
335
|
-
# if not attr.startswith("__"):
|
|
336
|
-
# setattr(wrapper, attr, getattr(func, attr))
|
|
337
|
-
|
|
338
340
|
return wrapper
|
|
339
341
|
|
|
340
342
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
from fastapi import Request, Response, WebSocket
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PresentationException(Exception):
|
|
9
|
+
"""Base exception for presentation layer errors."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
*,
|
|
14
|
+
original_exception: Exception,
|
|
15
|
+
request: Request | None = None,
|
|
16
|
+
response: Response | None = None,
|
|
17
|
+
websocket: WebSocket | None = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
super().__init__(str(original_exception))
|
|
20
|
+
self.original_exception = original_exception
|
|
21
|
+
self.request = request
|
|
22
|
+
self.response = response
|
|
23
|
+
self.websocket = websocket
|
jararaca/presentation/hooks.py
CHANGED
jararaca/presentation/server.py
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import signal
|
|
9
|
+
import threading
|
|
1
10
|
from contextlib import asynccontextmanager
|
|
11
|
+
from signal import SIGINT, SIGTERM
|
|
2
12
|
from typing import Any, AsyncGenerator
|
|
3
13
|
|
|
4
|
-
from fastapi import Depends, FastAPI, Request, WebSocket
|
|
14
|
+
from fastapi import Depends, FastAPI, HTTPException, Request, Response, WebSocket
|
|
5
15
|
from starlette.types import ASGIApp
|
|
6
16
|
|
|
7
17
|
from jararaca.core.uow import UnitOfWorkContextProvider
|
|
@@ -10,12 +20,18 @@ from jararaca.lifecycle import AppLifecycle
|
|
|
10
20
|
from jararaca.microservice import (
|
|
11
21
|
AppTransactionContext,
|
|
12
22
|
HttpTransactionData,
|
|
23
|
+
ShutdownState,
|
|
13
24
|
WebSocketTransactionData,
|
|
25
|
+
provide_shutdown_state,
|
|
26
|
+
providing_app_type,
|
|
14
27
|
)
|
|
15
28
|
from jararaca.presentation.decorators import RestController
|
|
29
|
+
from jararaca.presentation.exceptions import PresentationException
|
|
16
30
|
from jararaca.presentation.http_microservice import HttpMicroservice
|
|
17
31
|
from jararaca.reflect.controller_inspect import ControllerMemberReflect
|
|
18
32
|
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
19
35
|
|
|
20
36
|
class HttpAppLifecycle:
|
|
21
37
|
|
|
@@ -31,58 +47,83 @@ class HttpAppLifecycle:
|
|
|
31
47
|
|
|
32
48
|
@asynccontextmanager
|
|
33
49
|
async def __call__(self, api: FastAPI) -> AsyncGenerator[None, None]:
|
|
34
|
-
|
|
50
|
+
with providing_app_type("http"):
|
|
51
|
+
async with self.lifecycle():
|
|
35
52
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
# for interceptor in self.lifecycle.initialized_interceptors
|
|
39
|
-
# if isinstance(interceptor, WebSocketInterceptor)
|
|
40
|
-
# ]
|
|
53
|
+
for controller_t in self.lifecycle.app.controllers:
|
|
54
|
+
controller = RestController.get_last(controller_t)
|
|
41
55
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
# self.lifecycle.app, self.lifecycle.container, self.uow_provider
|
|
45
|
-
# )
|
|
56
|
+
if controller is None:
|
|
57
|
+
continue
|
|
46
58
|
|
|
47
|
-
|
|
59
|
+
instance: Any = self.lifecycle.container.get_by_type(controller_t)
|
|
48
60
|
|
|
49
|
-
|
|
50
|
-
controller = RestController.get_controller(controller_t)
|
|
61
|
+
router = controller.get_router_factory()(self.lifecycle, instance)
|
|
51
62
|
|
|
52
|
-
|
|
53
|
-
continue
|
|
63
|
+
api.include_router(router)
|
|
54
64
|
|
|
55
|
-
|
|
65
|
+
for middleware in self.http_app.middlewares:
|
|
66
|
+
middleware_instance = self.lifecycle.container.get_by_type(
|
|
67
|
+
middleware
|
|
68
|
+
)
|
|
69
|
+
api.router.dependencies.append(
|
|
70
|
+
Depends(middleware_instance.intercept)
|
|
71
|
+
)
|
|
56
72
|
|
|
57
|
-
|
|
58
|
-
# for middleware in controller.middlewares:
|
|
59
|
-
# middleware_instance = self.lifecycle.container.get_by_type(
|
|
60
|
-
# middleware
|
|
61
|
-
# )
|
|
62
|
-
# dependencies.append(Depends(middleware_instance.intercept))
|
|
73
|
+
yield
|
|
63
74
|
|
|
64
|
-
router = controller.get_router_factory()(self.lifecycle, instance)
|
|
65
75
|
|
|
66
|
-
|
|
76
|
+
class HttpShutdownState(ShutdownState):
|
|
77
|
+
def __init__(self) -> None:
|
|
78
|
+
self._requested = False
|
|
79
|
+
self.old_signal_handlers = {
|
|
80
|
+
SIGINT: signal.getsignal(SIGINT),
|
|
81
|
+
SIGTERM: signal.getsignal(SIGTERM),
|
|
82
|
+
}
|
|
83
|
+
self.thread_lock = threading.Lock()
|
|
84
|
+
self.aevent = asyncio.Event()
|
|
67
85
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
86
|
+
def request_shutdown(self) -> None:
|
|
87
|
+
if not self._requested:
|
|
88
|
+
self._requested = True
|
|
89
|
+
os.kill(os.getpid(), SIGINT)
|
|
90
|
+
|
|
91
|
+
def is_shutdown_requested(self) -> bool:
|
|
92
|
+
return self._requested
|
|
93
|
+
|
|
94
|
+
def handle_signal(self, signum: int, frame: Any) -> None:
|
|
95
|
+
logger.warning(f"Received signal {signum}, initiating shutdown...")
|
|
96
|
+
self.aevent.set()
|
|
97
|
+
if self._requested:
|
|
98
|
+
logger.warning("Shutdown already requested, ignoring signal.")
|
|
99
|
+
return
|
|
100
|
+
logger.warning("Requesting shutdown...")
|
|
101
|
+
self._requested = True
|
|
75
102
|
|
|
76
|
-
|
|
103
|
+
# remove the signal handler to prevent recursion
|
|
104
|
+
for sig in (SIGINT, SIGTERM):
|
|
105
|
+
if self.old_signal_handlers[sig] is not None:
|
|
106
|
+
signal.signal(sig, self.old_signal_handlers[sig])
|
|
107
|
+
|
|
108
|
+
signal.raise_signal(signum)
|
|
109
|
+
|
|
110
|
+
async def wait_for_shutdown(self) -> None:
|
|
111
|
+
await self.aevent.wait()
|
|
112
|
+
|
|
113
|
+
def setup_signal_handlers(self) -> None:
|
|
114
|
+
signal.signal(SIGINT, self.handle_signal)
|
|
115
|
+
signal.signal(SIGTERM, self.handle_signal)
|
|
77
116
|
|
|
78
117
|
|
|
79
118
|
class HttpUowContextProviderDependency:
|
|
80
119
|
|
|
81
120
|
def __init__(self, uow_provider: UnitOfWorkContextProvider) -> None:
|
|
82
121
|
self.uow_provider = uow_provider
|
|
122
|
+
self.shutdown_state = HttpShutdownState()
|
|
123
|
+
self.shutdown_state.setup_signal_handlers()
|
|
83
124
|
|
|
84
125
|
async def __call__(
|
|
85
|
-
self, websocket: WebSocket = None, request: Request = None # type: ignore
|
|
126
|
+
self, websocket: WebSocket = None, request: Request = None, response: Response = None # type: ignore
|
|
86
127
|
) -> AsyncGenerator[None, None]:
|
|
87
128
|
if request:
|
|
88
129
|
endpoint = request.scope["endpoint"]
|
|
@@ -101,17 +142,28 @@ class HttpUowContextProviderDependency:
|
|
|
101
142
|
"ControllerMemberReflect, but got: {}".format(type(member))
|
|
102
143
|
)
|
|
103
144
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
145
|
+
with provide_shutdown_state(self.shutdown_state):
|
|
146
|
+
async with self.uow_provider(
|
|
147
|
+
AppTransactionContext(
|
|
148
|
+
controller_member_reflect=member,
|
|
149
|
+
transaction_data=(
|
|
150
|
+
HttpTransactionData(request=request, response=response)
|
|
151
|
+
if request
|
|
152
|
+
else WebSocketTransactionData(websocket=websocket)
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
):
|
|
156
|
+
try:
|
|
157
|
+
yield
|
|
158
|
+
except HTTPException:
|
|
159
|
+
raise
|
|
160
|
+
except Exception as e:
|
|
161
|
+
raise PresentationException(
|
|
162
|
+
original_exception=e,
|
|
163
|
+
request=request,
|
|
164
|
+
response=response,
|
|
165
|
+
websocket=websocket,
|
|
166
|
+
)
|
|
115
167
|
|
|
116
168
|
|
|
117
169
|
def create_http_server(
|