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.
Files changed (91) hide show
  1. README.md +121 -0
  2. jararaca/__init__.py +267 -15
  3. jararaca/__main__.py +4 -0
  4. jararaca/broker_backend/__init__.py +106 -0
  5. jararaca/broker_backend/mapper.py +25 -0
  6. jararaca/broker_backend/redis_broker_backend.py +168 -0
  7. jararaca/cli.py +840 -103
  8. jararaca/common/__init__.py +3 -0
  9. jararaca/core/__init__.py +3 -0
  10. jararaca/core/providers.py +4 -0
  11. jararaca/core/uow.py +55 -16
  12. jararaca/di.py +4 -0
  13. jararaca/files/entity.py.mako +4 -0
  14. jararaca/lifecycle.py +6 -2
  15. jararaca/messagebus/__init__.py +5 -1
  16. jararaca/messagebus/bus_message_controller.py +4 -0
  17. jararaca/messagebus/consumers/__init__.py +3 -0
  18. jararaca/messagebus/decorators.py +90 -85
  19. jararaca/messagebus/implicit_headers.py +49 -0
  20. jararaca/messagebus/interceptors/__init__.py +3 -0
  21. jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +95 -37
  22. jararaca/messagebus/interceptors/publisher_interceptor.py +42 -0
  23. jararaca/messagebus/message.py +31 -0
  24. jararaca/messagebus/publisher.py +47 -4
  25. jararaca/messagebus/worker.py +1615 -135
  26. jararaca/microservice.py +248 -36
  27. jararaca/observability/constants.py +7 -0
  28. jararaca/observability/decorators.py +177 -16
  29. jararaca/observability/fastapi_exception_handler.py +37 -0
  30. jararaca/observability/hooks.py +109 -0
  31. jararaca/observability/interceptor.py +8 -2
  32. jararaca/observability/providers/__init__.py +3 -0
  33. jararaca/observability/providers/otel.py +213 -18
  34. jararaca/persistence/base.py +40 -3
  35. jararaca/persistence/exports.py +4 -0
  36. jararaca/persistence/interceptors/__init__.py +3 -0
  37. jararaca/persistence/interceptors/aiosqa_interceptor.py +187 -23
  38. jararaca/persistence/interceptors/constants.py +5 -0
  39. jararaca/persistence/interceptors/decorators.py +50 -0
  40. jararaca/persistence/session.py +3 -0
  41. jararaca/persistence/sort_filter.py +4 -0
  42. jararaca/persistence/utilities.py +74 -32
  43. jararaca/presentation/__init__.py +3 -0
  44. jararaca/presentation/decorators.py +170 -82
  45. jararaca/presentation/exceptions.py +23 -0
  46. jararaca/presentation/hooks.py +4 -0
  47. jararaca/presentation/http_microservice.py +4 -0
  48. jararaca/presentation/server.py +120 -41
  49. jararaca/presentation/websocket/__init__.py +3 -0
  50. jararaca/presentation/websocket/base_types.py +4 -0
  51. jararaca/presentation/websocket/context.py +34 -4
  52. jararaca/presentation/websocket/decorators.py +8 -41
  53. jararaca/presentation/websocket/redis.py +280 -53
  54. jararaca/presentation/websocket/types.py +6 -2
  55. jararaca/presentation/websocket/websocket_interceptor.py +74 -23
  56. jararaca/reflect/__init__.py +3 -0
  57. jararaca/reflect/controller_inspect.py +81 -0
  58. jararaca/reflect/decorators.py +238 -0
  59. jararaca/reflect/metadata.py +76 -0
  60. jararaca/rpc/__init__.py +3 -0
  61. jararaca/rpc/http/__init__.py +101 -0
  62. jararaca/rpc/http/backends/__init__.py +14 -0
  63. jararaca/rpc/http/backends/httpx.py +43 -9
  64. jararaca/rpc/http/backends/otel.py +4 -0
  65. jararaca/rpc/http/decorators.py +378 -113
  66. jararaca/rpc/http/httpx.py +3 -0
  67. jararaca/scheduler/__init__.py +3 -0
  68. jararaca/scheduler/beat_worker.py +758 -0
  69. jararaca/scheduler/decorators.py +89 -28
  70. jararaca/scheduler/types.py +11 -0
  71. jararaca/tools/app_config/__init__.py +3 -0
  72. jararaca/tools/app_config/decorators.py +7 -19
  73. jararaca/tools/app_config/interceptor.py +10 -4
  74. jararaca/tools/typescript/__init__.py +3 -0
  75. jararaca/tools/typescript/decorators.py +120 -0
  76. jararaca/tools/typescript/interface_parser.py +1126 -189
  77. jararaca/utils/__init__.py +3 -0
  78. jararaca/utils/rabbitmq_utils.py +372 -0
  79. jararaca/utils/retry.py +148 -0
  80. jararaca-0.4.0a5.dist-info/LICENSE +674 -0
  81. jararaca-0.4.0a5.dist-info/LICENSES/GPL-3.0-or-later.txt +232 -0
  82. {jararaca-0.2.37a12.dist-info → jararaca-0.4.0a5.dist-info}/METADATA +14 -7
  83. jararaca-0.4.0a5.dist-info/RECORD +88 -0
  84. {jararaca-0.2.37a12.dist-info → jararaca-0.4.0a5.dist-info}/WHEEL +1 -1
  85. pyproject.toml +131 -0
  86. jararaca/messagebus/types.py +0 -30
  87. jararaca/scheduler/scheduler.py +0 -154
  88. jararaca/tools/metadata.py +0 -47
  89. jararaca-0.2.37a12.dist-info/RECORD +0 -63
  90. /jararaca-0.2.37a12.dist-info/LICENSE → /LICENSE +0 -0
  91. {jararaca-0.2.37a12.dist-info → jararaca-0.4.0a5.dist-info}/entry_points.txt +0 -0
@@ -1,5 +1,12 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
1
5
  import inspect
2
- from typing import Any, Callable, Literal, Protocol, TypeVar, cast
6
+ from contextlib import contextmanager
7
+ from contextvars import ContextVar
8
+ from functools import wraps
9
+ from typing import Any, Awaitable, Callable, Literal, Mapping, Protocol, TypeVar
3
10
 
4
11
  from fastapi import APIRouter
5
12
  from fastapi import Depends as DependsF
@@ -9,12 +16,17 @@ from fastapi.params import Depends
9
16
  from jararaca.lifecycle import AppLifecycle
10
17
  from jararaca.presentation.http_microservice import HttpMiddleware
11
18
  from jararaca.presentation.websocket.decorators import WebSocketEndpoint
19
+ from jararaca.reflect.controller_inspect import (
20
+ ControllerMemberReflect,
21
+ inspect_controller,
22
+ )
23
+ from jararaca.reflect.decorators import DECORATED_T, StackableDecorator
12
24
 
25
+ DECORATED_TYPE = TypeVar("DECORATED_TYPE", bound=Any)
13
26
  DECORATED_FUNC = TypeVar("DECORATED_FUNC", bound=Callable[..., Any])
14
- DECORATED_CLASS = TypeVar("DECORATED_CLASS", bound=Any)
15
27
 
16
28
 
17
- ControllerOptions = dict[str, Any]
29
+ ControllerOptions = Mapping[str, Any]
18
30
 
19
31
 
20
32
  class RouterFactory(Protocol):
@@ -22,20 +34,25 @@ class RouterFactory(Protocol):
22
34
  def __call__(self, lifecycle: AppLifecycle, instance: Any) -> APIRouter: ...
23
35
 
24
36
 
25
- class RestController:
37
+ class RestController(StackableDecorator):
26
38
  REST_CONTROLLER_ATTR = "__rest_controller__"
27
39
 
28
40
  def __init__(
29
41
  self,
30
42
  path: str = "",
43
+ *,
31
44
  options: ControllerOptions | None = None,
32
45
  middlewares: list[type[HttpMiddleware]] = [],
33
46
  router_factory: RouterFactory | None = None,
47
+ class_inherits_decorators: bool = True,
48
+ methods_inherit_decorators: bool = True,
34
49
  ) -> None:
35
50
  self.path = path
36
51
  self.options = options
37
52
  self.router_factory = router_factory
38
53
  self.middlewares = middlewares
54
+ self.class_inherits_decorators = class_inherits_decorators
55
+ self.methods_inherit_decorators = methods_inherit_decorators
39
56
 
40
57
  def get_router_factory(
41
58
  self,
@@ -44,12 +61,15 @@ class RestController:
44
61
  raise Exception("Router factory is not set")
45
62
  return self.router_factory
46
63
 
47
- def __call__(self, cls: type[DECORATED_CLASS]) -> type[DECORATED_CLASS]:
64
+ def post_decorated(self, subject: DECORATED_T) -> None:
48
65
 
49
66
  def router_factory(
50
67
  lifecycle: AppLifecycle,
51
- instance: DECORATED_CLASS,
68
+ instance: DECORATED_T,
52
69
  ) -> APIRouter:
70
+ assert inspect.isclass(
71
+ subject
72
+ ), "RestController can only be applied to classes"
53
73
  dependencies: list[Depends] = []
54
74
 
55
75
  for self_middleware_type in self.middlewares:
@@ -58,13 +78,19 @@ class RestController:
58
78
  )
59
79
  dependencies.append(Depends(middleware_instance.intercept))
60
80
 
61
- for middlewares_by_hook in UseMiddleware.get_middlewares(instance):
81
+ class_middlewares = UseMiddleware.get_all_from_type(
82
+ subject, self.class_inherits_decorators
83
+ )
84
+ for middlewares_by_hook in class_middlewares:
62
85
  middleware_instance = lifecycle.container.get_by_type(
63
86
  middlewares_by_hook.middleware
64
87
  )
65
88
  dependencies.append(Depends(middleware_instance.intercept))
66
89
 
67
- for dependency in UseDependency.get_dependencies(instance):
90
+ class_dependencies = UseDependency.get_all_from_type(
91
+ subject, self.class_inherits_decorators
92
+ )
93
+ for dependency in class_dependencies:
68
94
  dependencies.append(DependsF(dependency.dependency))
69
95
 
70
96
  router = APIRouter(
@@ -73,15 +99,23 @@ class RestController:
73
99
  **(self.options or {}),
74
100
  )
75
101
 
76
- members = inspect.getmembers(cls, predicate=inspect.isfunction)
102
+ controller, members = inspect_controller(subject)
77
103
 
78
104
  router_members = [
79
- (name, mapping)
80
- for name, member in members
105
+ (name, mapping, member)
106
+ for name, member in members.items()
81
107
  if (
82
- mapping := (
83
- HttpMapping.get_http_mapping(member)
84
- or WebSocketEndpoint.get(member)
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,
85
119
  )
86
120
  )
87
121
  is not None
@@ -89,27 +123,38 @@ class RestController:
89
123
 
90
124
  router_members.sort(key=lambda x: x[1].order)
91
125
 
92
- for name, mapping in router_members:
126
+ for name, mapping, member in router_members:
93
127
  route_dependencies: list[Depends] = []
94
- for middlewares_by_hook in UseMiddleware.get_middlewares(
95
- getattr(instance, name)
96
- ):
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:
97
133
  middleware_instance = lifecycle.container.get_by_type(
98
134
  middlewares_by_hook.middleware
99
135
  )
100
136
  route_dependencies.append(Depends(middleware_instance.intercept))
101
137
 
102
- for dependency in UseDependency.get_dependencies(
103
- getattr(instance, name)
104
- ):
138
+ method_dependencies = UseDependency.get_all_from_method(
139
+ subject,
140
+ name,
141
+ self.methods_inherit_decorators,
142
+ )
143
+ for dependency in method_dependencies:
105
144
  route_dependencies.append(DependsF(dependency.dependency))
106
145
 
146
+ instance_method = getattr(instance, name)
147
+ instance_method = wraps_with_attributes(
148
+ instance_method,
149
+ controller_member_reflect=member,
150
+ )
151
+
107
152
  if isinstance(mapping, HttpMapping):
108
153
  try:
109
154
  router.add_api_route(
110
155
  methods=[mapping.method],
111
156
  path=mapping.path,
112
- endpoint=getattr(instance, name),
157
+ endpoint=instance_method,
113
158
  dependencies=route_dependencies,
114
159
  **(mapping.options or {}),
115
160
  )
@@ -120,7 +165,7 @@ class RestController:
120
165
  else:
121
166
  router.add_api_websocket_route(
122
167
  path=mapping.path,
123
- endpoint=getattr(instance, name),
168
+ endpoint=instance_method,
124
169
  dependencies=route_dependencies,
125
170
  **(mapping.options or {}),
126
171
  )
@@ -129,21 +174,6 @@ class RestController:
129
174
 
130
175
  self.router_factory = router_factory
131
176
 
132
- RestController.register(cls, self)
133
-
134
- return cls
135
-
136
- @staticmethod
137
- def register(cls: type[DECORATED_CLASS], controller: "RestController") -> None:
138
- setattr(cls, RestController.REST_CONTROLLER_ATTR, controller)
139
-
140
- @staticmethod
141
- def get_controller(cls: type[DECORATED_CLASS]) -> "RestController | None":
142
- if not hasattr(cls, RestController.REST_CONTROLLER_ATTR):
143
- return None
144
-
145
- return cast(RestController, getattr(cls, RestController.REST_CONTROLLER_ATTR))
146
-
147
177
 
148
178
  Options = dict[str, Any]
149
179
 
@@ -158,9 +188,8 @@ ResponseType = Literal[
158
188
  ]
159
189
 
160
190
 
161
- class HttpMapping:
191
+ class HttpMapping(StackableDecorator):
162
192
 
163
- HTTP_MAPPING_ATTR = "__http_mapping__"
164
193
  ORDER_COUNTER = 0
165
194
 
166
195
  def __init__(
@@ -178,24 +207,20 @@ class HttpMapping:
178
207
  HttpMapping.ORDER_COUNTER += 1
179
208
  self.order = HttpMapping.ORDER_COUNTER
180
209
 
181
- def __call__(self, func: DECORATED_FUNC) -> DECORATED_FUNC:
210
+ @classmethod
211
+ def decorator_key(cls) -> "type[HttpMapping]":
212
+ return HttpMapping
182
213
 
183
- HttpMapping.register(func, self)
214
+ # def __call__(self, func: DECORATED_FUNC) -> DECORATED_FUNC:
184
215
 
185
- return func
186
-
187
- @staticmethod
188
- def register(func: DECORATED_FUNC, mapping: "HttpMapping") -> None:
216
+ # HttpMapping.register(func, self)
189
217
 
190
- setattr(func, HttpMapping.HTTP_MAPPING_ATTR, mapping)
218
+ # return func
191
219
 
192
- @staticmethod
193
- def get_http_mapping(func: DECORATED_FUNC) -> "HttpMapping | None":
220
+ # @staticmethod
221
+ # def register(func: DECORATED_FUNC, mapping: "HttpMapping") -> None:
194
222
 
195
- if not hasattr(func, HttpMapping.HTTP_MAPPING_ATTR):
196
- return None
197
-
198
- return cast(HttpMapping, getattr(func, HttpMapping.HTTP_MAPPING_ATTR))
223
+ # setattr(func, HttpMapping.HTTP_MAPPING_ATTR, mapping)
199
224
 
200
225
 
201
226
  class Post(HttpMapping):
@@ -253,49 +278,112 @@ class Patch(HttpMapping):
253
278
  super().__init__("PATCH", path, options, response_type)
254
279
 
255
280
 
256
- class UseMiddleware:
257
-
258
- __MIDDLEWARES_ATTR__ = "__middlewares__"
281
+ class UseMiddleware(StackableDecorator):
259
282
 
260
283
  def __init__(self, middleware: type[HttpMiddleware]) -> None:
261
284
  self.middleware = middleware
262
285
 
263
- def __call__(self, subject: DECORATED_FUNC) -> DECORATED_FUNC:
264
286
 
265
- UseMiddleware.register(subject, self)
287
+ class UseDependency(StackableDecorator):
266
288
 
267
- return subject
289
+ def __init__(self, dependency: Any) -> None:
290
+ self.dependency = dependency
268
291
 
269
- @staticmethod
270
- def register(subject: DECORATED_FUNC, middleware: "UseMiddleware") -> None:
271
- middlewares = getattr(subject, UseMiddleware.__MIDDLEWARES_ATTR__, [])
272
- middlewares.append(middleware)
273
- setattr(subject, UseMiddleware.__MIDDLEWARES_ATTR__, middlewares)
274
292
 
275
- @staticmethod
276
- def get_middlewares(subject: DECORATED_FUNC) -> list["UseMiddleware"]:
277
- return getattr(subject, UseMiddleware.__MIDDLEWARES_ATTR__, [])
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.
278
299
 
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.
279
303
 
280
- class UseDependency:
304
+ Returns:
305
+ Callable[[DECORATED_TYPE], DECORATED_TYPE]: A single decorator that applies all the given decorators.
281
306
 
282
- __DEPENDENCY_ATTR__ = "__dependencies__"
307
+ Example:
308
+ IsWorkspaceAdmin = compose_route_decorators(
309
+ UseMiddleware(AuthMiddleware),
310
+ UseMiddleware(IsWorkspaceScoped),
311
+ UseMiddleware(RequiresAdminRole),
312
+ )
313
+ """
283
314
 
284
- def __init__(self, dependency: Any) -> None:
285
- self.dependency = dependency
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
321
+
322
+
323
+ def wraps_with_member_data(
324
+ controller_member: ControllerMemberReflect, func: Callable[..., Awaitable[Any]]
325
+ ) -> Callable[..., Any]:
326
+ """
327
+ A decorator that wraps a function and preserves its metadata.
328
+ This is useful for preserving metadata when using decorators.
329
+ """
330
+
331
+ @wraps(func)
332
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
333
+
334
+ with providing_controller_member(
335
+ controller_member=controller_member,
336
+ ):
337
+
338
+ return await func(*args, **kwargs)
339
+
340
+ return wrapper
341
+
342
+
343
+ controller_member_ctxvar = ContextVar[ControllerMemberReflect](
344
+ "controller_member_ctxvar"
345
+ )
346
+
347
+
348
+ @contextmanager
349
+ def providing_controller_member(
350
+ controller_member: ControllerMemberReflect,
351
+ ) -> Any:
352
+ """
353
+ Context manager to provide the controller member metadata.
354
+ This is used to preserve the metadata of the controller member
355
+ when using decorators.
356
+ """
357
+ token = controller_member_ctxvar.set(controller_member)
358
+ try:
359
+ yield
360
+ finally:
361
+ controller_member_ctxvar.reset(token)
362
+
363
+
364
+ def use_controller_member() -> ControllerMemberReflect:
365
+ """
366
+ Get the current controller member metadata.
367
+ This is used to access the metadata of the controller member
368
+ when using decorators.
369
+ """
370
+ return controller_member_ctxvar.get()
286
371
 
287
- def __call__(self, subject: DECORATED_FUNC) -> DECORATED_FUNC:
288
372
 
289
- UseDependency.register(subject, self)
373
+ def wraps_with_attributes(
374
+ func: Callable[..., Awaitable[Any]], **attributes: Any
375
+ ) -> Callable[..., Awaitable[Any]]:
376
+ """
377
+ A decorator that wraps a function and preserves its attributes.
378
+ This is useful for preserving attributes when using decorators.
379
+ """
290
380
 
291
- return subject
381
+ @wraps(func)
382
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
383
+ return await func(*args, **kwargs)
292
384
 
293
- @staticmethod
294
- def register(subject: DECORATED_FUNC, dependency: "UseDependency") -> None:
295
- dependencies = getattr(subject, UseDependency.__DEPENDENCY_ATTR__, [])
296
- dependencies.append(dependency)
297
- setattr(subject, UseDependency.__DEPENDENCY_ATTR__, dependencies)
385
+ # Copy attributes from the original function to the wrapper
386
+ for key, value in attributes.items():
387
+ setattr(wrapper, key, value)
298
388
 
299
- @staticmethod
300
- def get_dependencies(subject: DECORATED_FUNC) -> list["UseDependency"]:
301
- return getattr(subject, UseDependency.__DEPENDENCY_ATTR__, [])
389
+ return wrapper
@@ -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
@@ -1,3 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
1
5
  from contextlib import contextmanager
2
6
  from typing import Any, ContextManager, Generator, Type
3
7
 
@@ -1,3 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
1
5
  from dataclasses import dataclass, field
2
6
  from typing import Any, AsyncGenerator, Callable, Protocol
3
7
 
@@ -1,15 +1,36 @@
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
8
18
  from jararaca.di import Container
9
19
  from jararaca.lifecycle import AppLifecycle
10
- from jararaca.microservice import HttpAppContext, WebSocketAppContext
20
+ from jararaca.microservice import (
21
+ AppTransactionContext,
22
+ HttpTransactionData,
23
+ ShutdownState,
24
+ WebSocketTransactionData,
25
+ provide_shutdown_state,
26
+ providing_app_type,
27
+ )
11
28
  from jararaca.presentation.decorators import RestController
29
+ from jararaca.presentation.exceptions import PresentationException
12
30
  from jararaca.presentation.http_microservice import HttpMicroservice
31
+ from jararaca.reflect.controller_inspect import ControllerMemberReflect
32
+
33
+ logger = logging.getLogger(__name__)
13
34
 
14
35
 
15
36
  class HttpAppLifecycle:
@@ -26,65 +47,123 @@ class HttpAppLifecycle:
26
47
 
27
48
  @asynccontextmanager
28
49
  async def __call__(self, api: FastAPI) -> AsyncGenerator[None, None]:
29
- async with self.lifecycle():
50
+ with providing_app_type("http"):
51
+ async with self.lifecycle():
30
52
 
31
- # websocket_interceptors = [
32
- # interceptor
33
- # for interceptor in self.lifecycle.initialized_interceptors
34
- # if isinstance(interceptor, WebSocketInterceptor)
35
- # ]
53
+ for controller_t in self.lifecycle.app.controllers:
54
+ controller = RestController.get_last(controller_t)
36
55
 
37
- # for interceptor in websocket_interceptors:
38
- # router = interceptor.get_ws_router(
39
- # self.lifecycle.app, self.lifecycle.container, self.uow_provider
40
- # )
56
+ if controller is None:
57
+ continue
41
58
 
42
- # api.include_router(router)
59
+ instance: Any = self.lifecycle.container.get_by_type(controller_t)
43
60
 
44
- for controller_t in self.lifecycle.app.controllers:
45
- controller = RestController.get_controller(controller_t)
61
+ router = controller.get_router_factory()(self.lifecycle, instance)
46
62
 
47
- if controller is None:
48
- continue
63
+ api.include_router(router)
49
64
 
50
- instance: Any = self.lifecycle.container.get_by_type(controller_t)
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
+ )
51
72
 
52
- # dependencies: list[DependsCls] = []
53
- # for middleware in controller.middlewares:
54
- # middleware_instance = self.lifecycle.container.get_by_type(
55
- # middleware
56
- # )
57
- # dependencies.append(Depends(middleware_instance.intercept))
73
+ yield
58
74
 
59
- router = controller.get_router_factory()(self.lifecycle, instance)
60
75
 
61
- api.include_router(router)
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()
62
85
 
63
- for middleware in self.http_app.middlewares:
64
- middleware_instance = self.lifecycle.container.get_by_type(
65
- middleware
66
- )
67
- api.router.dependencies.append(
68
- Depends(middleware_instance.intercept)
69
- )
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
70
102
 
71
- yield
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)
72
116
 
73
117
 
74
118
  class HttpUowContextProviderDependency:
75
119
 
76
120
  def __init__(self, uow_provider: UnitOfWorkContextProvider) -> None:
77
121
  self.uow_provider = uow_provider
122
+ self.shutdown_state = HttpShutdownState()
123
+ self.shutdown_state.setup_signal_handlers()
78
124
 
79
125
  async def __call__(
80
- self, websocket: WebSocket = None, request: Request = None # type: ignore
126
+ self, websocket: WebSocket = None, request: Request = None, response: Response = None # type: ignore
81
127
  ) -> AsyncGenerator[None, None]:
82
- async with self.uow_provider(
83
- HttpAppContext(request=request)
84
- if request
85
- else WebSocketAppContext(websocket=websocket)
86
- ):
87
- yield
128
+ if request:
129
+ endpoint = request.scope["endpoint"]
130
+ elif websocket:
131
+ endpoint = websocket.scope["endpoint"]
132
+ else:
133
+ raise ValueError("Either request or websocket must be provided.")
134
+
135
+ member = getattr(endpoint, "controller_member_reflect", None)
136
+
137
+ if member is None:
138
+ raise ValueError("The endpoint does not have a controller member reflect.")
139
+
140
+ assert isinstance(member, ControllerMemberReflect), (
141
+ "Expected endpoint.controller_member_reflect to be of type "
142
+ "ControllerMemberReflect, but got: {}".format(type(member))
143
+ )
144
+
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
+ )
88
167
 
89
168
 
90
169
  def create_http_server(
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
@@ -1,3 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
1
5
  from typing import ClassVar
2
6
 
3
7
  from pydantic import BaseModel