jararaca 0.3.11a16__py3-none-any.whl → 0.4.0a19__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 +189 -17
- 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 +915 -51
- jararaca/common/__init__.py +3 -0
- jararaca/core/__init__.py +3 -0
- jararaca/core/providers.py +8 -0
- jararaca/core/uow.py +41 -7
- jararaca/di.py +4 -0
- jararaca/files/entity.py.mako +4 -0
- jararaca/helpers/__init__.py +3 -0
- jararaca/helpers/global_scheduler/__init__.py +3 -0
- jararaca/helpers/global_scheduler/config.py +21 -0
- jararaca/helpers/global_scheduler/controller.py +42 -0
- jararaca/helpers/global_scheduler/registry.py +32 -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 +121 -61
- jararaca/messagebus/implicit_headers.py +49 -0
- jararaca/messagebus/interceptors/__init__.py +3 -0
- jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +62 -11
- jararaca/messagebus/interceptors/message_publisher_collector.py +62 -0
- jararaca/messagebus/interceptors/publisher_interceptor.py +29 -3
- jararaca/messagebus/message.py +4 -0
- jararaca/messagebus/publisher.py +6 -0
- jararaca/messagebus/worker.py +1002 -459
- jararaca/microservice.py +113 -2
- 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 +225 -16
- jararaca/persistence/base.py +39 -3
- 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 +73 -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 +252 -0
- jararaca/reflect/helpers.py +18 -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 +380 -115
- 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 +1077 -174
- jararaca/utils/__init__.py +3 -0
- jararaca/utils/env_parse_utils.py +133 -0
- jararaca/utils/rabbitmq_utils.py +112 -39
- jararaca/utils/retry.py +19 -14
- jararaca-0.4.0a19.dist-info/LICENSE +674 -0
- jararaca-0.4.0a19.dist-info/LICENSES/GPL-3.0-or-later.txt +232 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a19.dist-info}/METADATA +12 -7
- jararaca-0.4.0a19.dist-info/RECORD +96 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a19.dist-info}/WHEEL +1 -1
- pyproject.toml +132 -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.0a19.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,20 @@ 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,
|
|
324
|
+
final_listing_statement: (
|
|
325
|
+
Callable[[Select[Tuple[QUERY_ENTITY_T]]], Select[Tuple[QUERY_ENTITY_T]]]
|
|
326
|
+
| None
|
|
327
|
+
) = None,
|
|
328
|
+
total_type: Literal["total_over", "count_subquery"] = "total_over",
|
|
309
329
|
) -> "Paginated[QUERY_ENTITY_T]":
|
|
310
330
|
"""
|
|
311
331
|
Executes a query with the provided filter and interceptors.
|
|
@@ -316,8 +336,15 @@ class QueryOperations(Generic[QUERY_FILTER_T, QUERY_ENTITY_T]):
|
|
|
316
336
|
Paginated[QUERY_ENTITY_T]: A paginated result containing the items and metadata.
|
|
317
337
|
"""
|
|
318
338
|
|
|
339
|
+
initial_statement = self.base_statement or select(self.entity_type)
|
|
340
|
+
|
|
341
|
+
if base_statement is not None and callable(base_statement):
|
|
342
|
+
initial_statement = base_statement(initial_statement)
|
|
343
|
+
elif base_statement is not None and isinstance(base_statement, Select):
|
|
344
|
+
initial_statement = base_statement
|
|
345
|
+
|
|
319
346
|
tier_one_filtered_query = self.generate_filtered_query(
|
|
320
|
-
filter,
|
|
347
|
+
filter, initial_statement
|
|
321
348
|
)
|
|
322
349
|
|
|
323
350
|
tier_two_filtered_query = reduce(
|
|
@@ -340,30 +367,56 @@ class QueryOperations(Generic[QUERY_FILTER_T, QUERY_ENTITY_T]):
|
|
|
340
367
|
)
|
|
341
368
|
)
|
|
342
369
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
select(func.count()).select_from(tier_two_filtered_query.subquery())
|
|
346
|
-
)
|
|
347
|
-
).scalar_one()
|
|
370
|
+
if final_listing_statement is not None:
|
|
371
|
+
tier_two_filtered_query = final_listing_statement(tier_two_filtered_query)
|
|
348
372
|
|
|
349
|
-
|
|
350
|
-
(
|
|
351
|
-
|
|
373
|
+
if total_type == "total_over":
|
|
374
|
+
# Use window function for total count (single query)
|
|
375
|
+
paginated_query = tier_two_filtered_query.add_columns(
|
|
376
|
+
func.count().over().label("total_count")
|
|
377
|
+
)
|
|
378
|
+
paginated_query = paginated_query.limit(filter.page_size).offset(
|
|
379
|
+
(filter.page) * filter.page_size
|
|
380
|
+
)
|
|
352
381
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
382
|
+
result = await self.session.execute(paginated_query)
|
|
383
|
+
result = self.judge_unique(result)
|
|
384
|
+
rows = result.all()
|
|
385
|
+
|
|
386
|
+
if rows:
|
|
387
|
+
unpaginated_total = rows[0].total_count
|
|
388
|
+
result_scalars = [row[0] for row in rows]
|
|
389
|
+
else:
|
|
390
|
+
result_scalars = []
|
|
391
|
+
unpaginated_total = 0
|
|
392
|
+
else: # total_type == "count_subquery"
|
|
393
|
+
# Use separate count query (two queries)
|
|
394
|
+
paginated_query = tier_two_filtered_query.limit(filter.page_size).offset(
|
|
395
|
+
(filter.page) * filter.page_size
|
|
356
396
|
)
|
|
357
|
-
).scalar_one()
|
|
358
397
|
|
|
359
|
-
|
|
360
|
-
|
|
398
|
+
result = await self.session.execute(paginated_query)
|
|
399
|
+
result = self.judge_unique(result)
|
|
400
|
+
result_scalars = list(result.scalars())
|
|
401
|
+
|
|
402
|
+
# Always fetch total with separate query
|
|
403
|
+
unpaginated_total = (
|
|
404
|
+
await self.session.execute(
|
|
405
|
+
tier_two_filtered_query.with_only_columns(
|
|
406
|
+
func.count(self.entity_type.id)
|
|
407
|
+
).order_by(None)
|
|
408
|
+
if issubclass(self.entity_type, IdentifiableEntity)
|
|
409
|
+
else select(func.count()).select_from(
|
|
410
|
+
tier_two_filtered_query.subquery()
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
).scalar_one()
|
|
361
414
|
|
|
362
415
|
return Paginated(
|
|
363
416
|
items=result_scalars,
|
|
364
|
-
total=
|
|
417
|
+
total=len(result_scalars),
|
|
365
418
|
unpaginated_total=unpaginated_total,
|
|
366
|
-
total_pages=
|
|
419
|
+
total_pages=math.ceil(unpaginated_total / filter.page_size),
|
|
367
420
|
)
|
|
368
421
|
|
|
369
422
|
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 FUNC_OR_TYPE_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: FUNC_OR_TYPE_T) -> None:
|
|
54
65
|
|
|
55
66
|
def router_factory(
|
|
56
67
|
lifecycle: AppLifecycle,
|
|
57
|
-
instance:
|
|
68
|
+
instance: FUNC_OR_TYPE_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