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.
Files changed (88) hide show
  1. README.md +121 -0
  2. jararaca/__init__.py +184 -12
  3. jararaca/__main__.py +4 -0
  4. jararaca/broker_backend/__init__.py +4 -0
  5. jararaca/broker_backend/mapper.py +4 -0
  6. jararaca/broker_backend/redis_broker_backend.py +9 -3
  7. jararaca/cli.py +272 -47
  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 +41 -7
  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 +4 -0
  16. jararaca/messagebus/bus_message_controller.py +4 -0
  17. jararaca/messagebus/consumers/__init__.py +3 -0
  18. jararaca/messagebus/decorators.py +33 -67
  19. jararaca/messagebus/implicit_headers.py +49 -0
  20. jararaca/messagebus/interceptors/__init__.py +3 -0
  21. jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +13 -4
  22. jararaca/messagebus/interceptors/publisher_interceptor.py +4 -0
  23. jararaca/messagebus/message.py +4 -0
  24. jararaca/messagebus/publisher.py +6 -0
  25. jararaca/messagebus/worker.py +850 -383
  26. jararaca/microservice.py +110 -1
  27. jararaca/observability/constants.py +7 -0
  28. jararaca/observability/decorators.py +170 -13
  29. jararaca/observability/fastapi_exception_handler.py +37 -0
  30. jararaca/observability/hooks.py +109 -0
  31. jararaca/observability/interceptor.py +4 -0
  32. jararaca/observability/providers/__init__.py +3 -0
  33. jararaca/observability/providers/otel.py +202 -11
  34. jararaca/persistence/base.py +38 -2
  35. jararaca/persistence/exports.py +4 -0
  36. jararaca/persistence/interceptors/__init__.py +3 -0
  37. jararaca/persistence/interceptors/aiosqa_interceptor.py +86 -73
  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 +50 -20
  43. jararaca/presentation/__init__.py +3 -0
  44. jararaca/presentation/decorators.py +88 -86
  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 +97 -45
  49. jararaca/presentation/websocket/__init__.py +3 -0
  50. jararaca/presentation/websocket/base_types.py +4 -0
  51. jararaca/presentation/websocket/context.py +4 -0
  52. jararaca/presentation/websocket/decorators.py +8 -41
  53. jararaca/presentation/websocket/redis.py +280 -53
  54. jararaca/presentation/websocket/types.py +4 -0
  55. jararaca/presentation/websocket/websocket_interceptor.py +46 -19
  56. jararaca/reflect/__init__.py +3 -0
  57. jararaca/reflect/controller_inspect.py +16 -10
  58. jararaca/reflect/decorators.py +238 -0
  59. jararaca/reflect/metadata.py +34 -25
  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 +521 -105
  69. jararaca/scheduler/decorators.py +15 -22
  70. jararaca/scheduler/types.py +4 -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 +6 -2
  74. jararaca/tools/typescript/__init__.py +3 -0
  75. jararaca/tools/typescript/decorators.py +120 -0
  76. jararaca/tools/typescript/interface_parser.py +1074 -173
  77. jararaca/utils/__init__.py +3 -0
  78. jararaca/utils/rabbitmq_utils.py +65 -39
  79. jararaca/utils/retry.py +10 -3
  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.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/METADATA +11 -7
  83. jararaca-0.4.0a5.dist-info/RECORD +88 -0
  84. {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/WHEEL +1 -1
  85. pyproject.toml +131 -0
  86. jararaca-0.3.11a16.dist-info/RECORD +0 -74
  87. /jararaca-0.3.11a16.dist-info/LICENSE → /LICENSE +0 -0
  88. {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/entry_points.txt +0 -0
@@ -1,21 +1,22 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+
1
6
  import inspect
2
7
  from dataclasses import dataclass
3
- from typing import Any, Awaitable, Callable, Generic, TypeVar, cast
8
+ from typing import Any, Awaitable, Callable
4
9
 
5
- from jararaca.messagebus.message import INHERITS_MESSAGE_CO, Message, MessageOf
10
+ from jararaca.messagebus.message import INHERITS_MESSAGE_CO
6
11
  from jararaca.reflect.controller_inspect import (
7
12
  ControllerMemberReflect,
8
13
  inspect_controller,
9
14
  )
15
+ from jararaca.reflect.decorators import DECORATED_T, StackableDecorator
10
16
  from jararaca.scheduler.decorators import ScheduledAction, ScheduledActionData
11
17
 
12
- DECORATED_FUNC = TypeVar("DECORATED_FUNC", bound=Callable[..., Any])
13
- DECORATED_T = TypeVar("DECORATED_T", bound=Any)
14
-
15
18
 
16
- class MessageHandler(Generic[INHERITS_MESSAGE_CO]):
17
-
18
- MESSAGE_INCOMING_ATTR = "__message_incoming__"
19
+ class MessageHandler(StackableDecorator):
19
20
 
20
21
  def __init__(
21
22
  self,
@@ -23,50 +24,23 @@ class MessageHandler(Generic[INHERITS_MESSAGE_CO]):
23
24
  timeout: int | None = None,
24
25
  exception_handler: Callable[[BaseException], None] | None = None,
25
26
  nack_on_exception: bool = False,
26
- auto_ack: bool = True,
27
+ auto_ack: bool = False,
27
28
  name: str | None = None,
28
29
  ) -> None:
29
30
  self.message_type = message
30
31
 
31
32
  self.timeout = timeout
32
33
  self.exception_handler = exception_handler
33
- self.requeue_on_exception = nack_on_exception
34
+ self.nack_on_exception = nack_on_exception
34
35
  self.auto_ack = auto_ack
35
36
  self.name = name
36
37
 
37
- def __call__(
38
- self, func: Callable[[Any, MessageOf[INHERITS_MESSAGE_CO]], Awaitable[None]]
39
- ) -> Callable[[Any, MessageOf[INHERITS_MESSAGE_CO]], Awaitable[None]]:
40
-
41
- MessageHandler[Any].register(func, self)
42
-
43
- return func
44
-
45
- @staticmethod
46
- def register(
47
- func: Callable[[Any, MessageOf[INHERITS_MESSAGE_CO]], Awaitable[None]],
48
- message_incoming: "MessageHandler[Any]",
49
- ) -> None:
50
-
51
- setattr(func, MessageHandler.MESSAGE_INCOMING_ATTR, message_incoming)
52
-
53
- @staticmethod
54
- def get_message_incoming(
55
- func: Callable[[MessageOf[Any]], Awaitable[Any]],
56
- ) -> "MessageHandler[Message] | None":
57
- if not hasattr(func, MessageHandler.MESSAGE_INCOMING_ATTR):
58
- return None
59
-
60
- return cast(
61
- MessageHandler[Message], getattr(func, MessageHandler.MESSAGE_INCOMING_ATTR)
62
- )
63
-
64
38
 
65
39
  @dataclass(frozen=True)
66
40
  class MessageHandlerData:
67
41
  message_type: type[Any]
68
- spec: MessageHandler[Message]
69
- instance_callable: Callable[[MessageOf[Any]], Awaitable[None]]
42
+ spec: MessageHandler
43
+ instance_callable: Callable[..., Awaitable[None]]
70
44
  controller_member: ControllerMemberReflect
71
45
 
72
46
 
@@ -80,16 +54,22 @@ SCHEDULED_ACTION_DATA_SET = set[ScheduledActionData]
80
54
  MESSAGE_HANDLER_DATA_SET = set[MessageHandlerData]
81
55
 
82
56
 
83
- class MessageBusController:
57
+ class MessageBusController(StackableDecorator):
84
58
 
85
- MESSAGEBUS_ATTR = "__messagebus__"
86
-
87
- def __init__(self) -> None:
59
+ def __init__(
60
+ self,
61
+ *,
62
+ inherit_class_decorators: bool = True,
63
+ inherit_methods_decorators: bool = True,
64
+ ) -> None:
88
65
  self.messagebus_factory: (
89
66
  Callable[[Any], tuple[MESSAGE_HANDLER_DATA_SET, SCHEDULED_ACTION_DATA_SET]]
90
67
  | None
91
68
  ) = None
92
69
 
70
+ self.inherit_class_decorators = inherit_class_decorators
71
+ self.inherit_methods_decorators = inherit_methods_decorators
72
+
93
73
  def get_messagebus_factory(
94
74
  self,
95
75
  ) -> Callable[
@@ -99,7 +79,7 @@ class MessageBusController:
99
79
  raise Exception("MessageBus factory is not set")
100
80
  return self.messagebus_factory
101
81
 
102
- def __call__(self, cls_t: type[DECORATED_T]) -> type[DECORATED_T]:
82
+ def post_decorated(self, subject: DECORATED_T) -> None:
103
83
 
104
84
  def messagebus_factory(
105
85
  instance: DECORATED_T,
@@ -108,13 +88,17 @@ class MessageBusController:
108
88
 
109
89
  schedulers: SCHEDULED_ACTION_DATA_SET = set()
110
90
 
111
- _, members = inspect_controller(cls_t)
91
+ assert inspect.isclass(
92
+ subject
93
+ ), "MessageBusController can only be applied to classes"
94
+
95
+ _, members = inspect_controller(subject)
112
96
 
113
97
  for name, member in members.items():
114
- message_handler_decoration = MessageHandler.get_message_incoming(
98
+ message_handler_decoration = MessageHandler.get_last(
115
99
  member.member_function
116
100
  )
117
- scheduled_action_decoration = ScheduledAction.get_scheduled_action(
101
+ scheduled_action_decoration = ScheduledAction.get_last(
118
102
  member.member_function
119
103
  )
120
104
 
@@ -123,7 +107,7 @@ class MessageBusController:
123
107
  if not inspect.iscoroutinefunction(member.member_function):
124
108
  raise Exception(
125
109
  "Message incoming handler '%s' from '%s.%s' must be a coroutine function"
126
- % (name, cls_t.__module__, cls_t.__qualname__)
110
+ % (name, subject.__module__, subject.__qualname__)
127
111
  )
128
112
 
129
113
  handlers.add(
@@ -138,7 +122,7 @@ class MessageBusController:
138
122
  if not inspect.iscoroutinefunction(member.member_function):
139
123
  raise Exception(
140
124
  "Scheduled action handler '%s' from '%s.%s' must be a coroutine function"
141
- % (name, cls_t.__module__, cls_t.__qualname__)
125
+ % (name, subject.__module__, subject.__qualname__)
142
126
  )
143
127
  instance_callable = getattr(instance, name)
144
128
 
@@ -153,21 +137,3 @@ class MessageBusController:
153
137
  return handlers, schedulers
154
138
 
155
139
  self.messagebus_factory = messagebus_factory
156
-
157
- MessageBusController.register(cls_t, self)
158
-
159
- return cls_t
160
-
161
- @staticmethod
162
- def register(func: type[DECORATED_T], messagebus: "MessageBusController") -> None:
163
-
164
- setattr(func, MessageBusController.MESSAGEBUS_ATTR, messagebus)
165
-
166
- @staticmethod
167
- def get_messagebus(func: type[DECORATED_T]) -> "MessageBusController | None":
168
- if not hasattr(func, MessageBusController.MESSAGEBUS_ATTR):
169
- return None
170
-
171
- return cast(
172
- MessageBusController, getattr(func, MessageBusController.MESSAGEBUS_ATTR)
173
- )
@@ -0,0 +1,49 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import datetime
6
+ import decimal
7
+ import typing
8
+ from contextlib import contextmanager
9
+ from contextvars import ContextVar
10
+ from typing import Any, Dict, Generator
11
+
12
+ FieldArray = list["FieldValue"]
13
+ """A data structure for holding an array of field values."""
14
+
15
+ FieldTable = typing.Dict[str, "FieldValue"]
16
+ FieldValue = (
17
+ bool
18
+ | bytes
19
+ | bytearray
20
+ | decimal.Decimal
21
+ | FieldArray
22
+ | FieldTable
23
+ | float
24
+ | int
25
+ | None
26
+ | str
27
+ | datetime.datetime
28
+ )
29
+
30
+ ImplicitHeaders = Dict[str, FieldValue]
31
+
32
+ implicit_headers_ctx = ContextVar[ImplicitHeaders | None](
33
+ "implicit_headers_ctx", default=None
34
+ )
35
+
36
+
37
+ def use_implicit_headers() -> ImplicitHeaders | None:
38
+ return implicit_headers_ctx.get()
39
+
40
+
41
+ @contextmanager
42
+ def provide_implicit_headers(
43
+ implicit_headers: ImplicitHeaders,
44
+ ) -> Generator[None, Any, None]:
45
+ token = implicit_headers_ctx.set(implicit_headers)
46
+ try:
47
+ yield
48
+ finally:
49
+ implicit_headers_ctx.reset(token)
@@ -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
  import logging
2
6
  from contextlib import asynccontextmanager
3
7
  from datetime import datetime, timedelta
@@ -9,6 +13,7 @@ from aio_pika.abc import AbstractConnection
9
13
  from pydantic import BaseModel
10
14
 
11
15
  from jararaca.broker_backend import MessageBrokerBackend
16
+ from jararaca.messagebus import implicit_headers
12
17
  from jararaca.messagebus.interceptors.publisher_interceptor import (
13
18
  MessageBusConnectionFactory,
14
19
  )
@@ -41,9 +46,13 @@ class AIOPikaMessagePublisher(MessagePublisher):
41
46
  if not exchange:
42
47
  logging.warning(f"Exchange {self.exchange_name} not found")
43
48
  return
44
- routing_key = f"{topic}."
49
+ routing_key = f"{topic}.#"
50
+
51
+ implicit_headers_data = implicit_headers.use_implicit_headers()
45
52
  await exchange.publish(
46
- aio_pika.Message(body=message.model_dump_json().encode()),
53
+ aio_pika.Message(
54
+ body=message.model_dump_json().encode(), headers=implicit_headers_data
55
+ ),
47
56
  routing_key=routing_key,
48
57
  )
49
58
 
@@ -122,7 +131,7 @@ class AIOPikaConnectionFactory(MessageBusConnectionFactory):
122
131
  if connection_pool_config:
123
132
 
124
133
  async def get_connection() -> AbstractConnection:
125
- return await aio_pika.connect_robust(self.url)
134
+ return await aio_pika.connect(self.url)
126
135
 
127
136
  self.connection_pool = aio_pika.pool.Pool[AbstractConnection](
128
137
  get_connection,
@@ -142,7 +151,7 @@ class AIOPikaConnectionFactory(MessageBusConnectionFactory):
142
151
  @asynccontextmanager
143
152
  async def acquire_connection(self) -> AsyncGenerator[AbstractConnection, Any]:
144
153
  if not self.connection_pool:
145
- async with await aio_pika.connect_robust(self.url) as connection:
154
+ async with await aio_pika.connect(self.url) as connection:
146
155
  yield connection
147
156
  else:
148
157
 
@@ -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 asynccontextmanager
2
6
  from typing import AsyncContextManager, AsyncGenerator, Protocol
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 datetime import datetime, tzinfo
2
6
  from typing import Generic, Protocol, TypeVar
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 abc import ABC, abstractmethod
2
6
  from contextlib import contextmanager, suppress
3
7
  from contextvars import ContextVar
@@ -19,6 +23,8 @@ class IMessage(BaseModel):
19
23
 
20
24
  MESSAGE_TYPE: ClassVar[Literal["task", "event"]] = "task"
21
25
 
26
+ MESSAGE_CATEGORY: ClassVar[str] = "uncategorized"
27
+
22
28
 
23
29
  class MessagePublisher(ABC):
24
30
  @abstractmethod