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.
Files changed (96) hide show
  1. README.md +121 -0
  2. jararaca/__init__.py +189 -17
  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 +915 -51
  8. jararaca/common/__init__.py +3 -0
  9. jararaca/core/__init__.py +3 -0
  10. jararaca/core/providers.py +8 -0
  11. jararaca/core/uow.py +41 -7
  12. jararaca/di.py +4 -0
  13. jararaca/files/entity.py.mako +4 -0
  14. jararaca/helpers/__init__.py +3 -0
  15. jararaca/helpers/global_scheduler/__init__.py +3 -0
  16. jararaca/helpers/global_scheduler/config.py +21 -0
  17. jararaca/helpers/global_scheduler/controller.py +42 -0
  18. jararaca/helpers/global_scheduler/registry.py +32 -0
  19. jararaca/lifecycle.py +6 -2
  20. jararaca/messagebus/__init__.py +4 -0
  21. jararaca/messagebus/bus_message_controller.py +4 -0
  22. jararaca/messagebus/consumers/__init__.py +3 -0
  23. jararaca/messagebus/decorators.py +121 -61
  24. jararaca/messagebus/implicit_headers.py +49 -0
  25. jararaca/messagebus/interceptors/__init__.py +3 -0
  26. jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +62 -11
  27. jararaca/messagebus/interceptors/message_publisher_collector.py +62 -0
  28. jararaca/messagebus/interceptors/publisher_interceptor.py +29 -3
  29. jararaca/messagebus/message.py +4 -0
  30. jararaca/messagebus/publisher.py +6 -0
  31. jararaca/messagebus/worker.py +1002 -459
  32. jararaca/microservice.py +113 -2
  33. jararaca/observability/constants.py +7 -0
  34. jararaca/observability/decorators.py +170 -13
  35. jararaca/observability/fastapi_exception_handler.py +37 -0
  36. jararaca/observability/hooks.py +109 -0
  37. jararaca/observability/interceptor.py +4 -0
  38. jararaca/observability/providers/__init__.py +3 -0
  39. jararaca/observability/providers/otel.py +225 -16
  40. jararaca/persistence/base.py +39 -3
  41. jararaca/persistence/exports.py +4 -0
  42. jararaca/persistence/interceptors/__init__.py +3 -0
  43. jararaca/persistence/interceptors/aiosqa_interceptor.py +86 -73
  44. jararaca/persistence/interceptors/constants.py +5 -0
  45. jararaca/persistence/interceptors/decorators.py +50 -0
  46. jararaca/persistence/session.py +3 -0
  47. jararaca/persistence/sort_filter.py +4 -0
  48. jararaca/persistence/utilities.py +73 -20
  49. jararaca/presentation/__init__.py +3 -0
  50. jararaca/presentation/decorators.py +88 -86
  51. jararaca/presentation/exceptions.py +23 -0
  52. jararaca/presentation/hooks.py +4 -0
  53. jararaca/presentation/http_microservice.py +4 -0
  54. jararaca/presentation/server.py +97 -45
  55. jararaca/presentation/websocket/__init__.py +3 -0
  56. jararaca/presentation/websocket/base_types.py +4 -0
  57. jararaca/presentation/websocket/context.py +4 -0
  58. jararaca/presentation/websocket/decorators.py +8 -41
  59. jararaca/presentation/websocket/redis.py +280 -53
  60. jararaca/presentation/websocket/types.py +4 -0
  61. jararaca/presentation/websocket/websocket_interceptor.py +46 -19
  62. jararaca/reflect/__init__.py +3 -0
  63. jararaca/reflect/controller_inspect.py +16 -10
  64. jararaca/reflect/decorators.py +252 -0
  65. jararaca/reflect/helpers.py +18 -0
  66. jararaca/reflect/metadata.py +34 -25
  67. jararaca/rpc/__init__.py +3 -0
  68. jararaca/rpc/http/__init__.py +101 -0
  69. jararaca/rpc/http/backends/__init__.py +14 -0
  70. jararaca/rpc/http/backends/httpx.py +43 -9
  71. jararaca/rpc/http/backends/otel.py +4 -0
  72. jararaca/rpc/http/decorators.py +380 -115
  73. jararaca/rpc/http/httpx.py +3 -0
  74. jararaca/scheduler/__init__.py +3 -0
  75. jararaca/scheduler/beat_worker.py +521 -105
  76. jararaca/scheduler/decorators.py +15 -22
  77. jararaca/scheduler/types.py +4 -0
  78. jararaca/tools/app_config/__init__.py +3 -0
  79. jararaca/tools/app_config/decorators.py +7 -19
  80. jararaca/tools/app_config/interceptor.py +6 -2
  81. jararaca/tools/typescript/__init__.py +3 -0
  82. jararaca/tools/typescript/decorators.py +120 -0
  83. jararaca/tools/typescript/interface_parser.py +1077 -174
  84. jararaca/utils/__init__.py +3 -0
  85. jararaca/utils/env_parse_utils.py +133 -0
  86. jararaca/utils/rabbitmq_utils.py +112 -39
  87. jararaca/utils/retry.py +19 -14
  88. jararaca-0.4.0a19.dist-info/LICENSE +674 -0
  89. jararaca-0.4.0a19.dist-info/LICENSES/GPL-3.0-or-later.txt +232 -0
  90. {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a19.dist-info}/METADATA +12 -7
  91. jararaca-0.4.0a19.dist-info/RECORD +96 -0
  92. {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a19.dist-info}/WHEEL +1 -1
  93. pyproject.toml +132 -0
  94. jararaca-0.3.11a16.dist-info/RECORD +0 -74
  95. /jararaca-0.3.11a16.dist-info/LICENSE → /LICENSE +0 -0
  96. {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a19.dist-info}/entry_points.txt +0 -0
@@ -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
@@ -5,10 +9,12 @@ from datetime import tzinfo as _TzInfo
5
9
  from typing import Any, AsyncGenerator
6
10
 
7
11
  import aio_pika
12
+ import tenacity
8
13
  from aio_pika.abc import AbstractConnection
9
14
  from pydantic import BaseModel
10
15
 
11
16
  from jararaca.broker_backend import MessageBrokerBackend
17
+ from jararaca.messagebus import implicit_headers
12
18
  from jararaca.messagebus.interceptors.publisher_interceptor import (
13
19
  MessageBusConnectionFactory,
14
20
  )
@@ -41,9 +47,13 @@ class AIOPikaMessagePublisher(MessagePublisher):
41
47
  if not exchange:
42
48
  logging.warning(f"Exchange {self.exchange_name} not found")
43
49
  return
44
- routing_key = f"{topic}."
50
+ routing_key = f"{topic}.#"
51
+
52
+ implicit_headers_data = implicit_headers.use_implicit_headers()
45
53
  await exchange.publish(
46
- aio_pika.Message(body=message.model_dump_json().encode()),
54
+ aio_pika.Message(
55
+ body=message.model_dump_json().encode(), headers=implicit_headers_data
56
+ ),
47
57
  routing_key=routing_key,
48
58
  )
49
59
 
@@ -62,7 +72,9 @@ class AIOPikaMessagePublisher(MessagePublisher):
62
72
  )
63
73
  )
64
74
 
65
- async def schedule(self, message: IMessage, when: datetime, tz: _TzInfo) -> None:
75
+ async def schedule(
76
+ self, message: IMessage, when: datetime, timezone: _TzInfo
77
+ ) -> None:
66
78
  if not self.message_broker_backend:
67
79
  raise NotImplementedError(
68
80
  "Schedule is not implemented for AIOPikaMessagePublisher"
@@ -101,6 +113,18 @@ class GenericPoolConfig(BaseModel):
101
113
  max_size: int
102
114
 
103
115
 
116
+ default_retry = tenacity.retry(
117
+ wait=tenacity.wait_exponential_jitter(initial=1, max=60),
118
+ before_sleep=tenacity.before_sleep_log(logger, logging.WARNING),
119
+ )
120
+
121
+ default_retry_channel = tenacity.retry(
122
+ wait=tenacity.wait_exponential_jitter(initial=1, max=60),
123
+ before_sleep=tenacity.before_sleep_log(logger, logging.WARNING),
124
+ stop=tenacity.stop_after_attempt(5),
125
+ )
126
+
127
+
104
128
  class AIOPikaConnectionFactory(MessageBusConnectionFactory):
105
129
 
106
130
  def __init__(
@@ -121,8 +145,9 @@ class AIOPikaConnectionFactory(MessageBusConnectionFactory):
121
145
 
122
146
  if connection_pool_config:
123
147
 
148
+ @default_retry
124
149
  async def get_connection() -> AbstractConnection:
125
- return await aio_pika.connect_robust(self.url)
150
+ return await aio_pika.connect(self.url)
126
151
 
127
152
  self.connection_pool = aio_pika.pool.Pool[AbstractConnection](
128
153
  get_connection,
@@ -131,6 +156,7 @@ class AIOPikaConnectionFactory(MessageBusConnectionFactory):
131
156
 
132
157
  if channel_pool_config:
133
158
 
159
+ @default_retry_channel
134
160
  async def get_channel() -> aio_pika.abc.AbstractChannel:
135
161
  async with self.acquire_connection() as connection:
136
162
  return await connection.channel(publisher_confirms=False)
@@ -139,10 +165,14 @@ class AIOPikaConnectionFactory(MessageBusConnectionFactory):
139
165
  get_channel, max_size=channel_pool_config.max_size
140
166
  )
141
167
 
168
+ @default_retry
169
+ async def _connect(self) -> AbstractConnection:
170
+ return await aio_pika.connect(self.url)
171
+
142
172
  @asynccontextmanager
143
173
  async def acquire_connection(self) -> AsyncGenerator[AbstractConnection, Any]:
144
174
  if not self.connection_pool:
145
- async with await aio_pika.connect_robust(self.url) as connection:
175
+ async with await self._connect() as connection:
146
176
  yield connection
147
177
  else:
148
178
 
@@ -153,12 +183,33 @@ class AIOPikaConnectionFactory(MessageBusConnectionFactory):
153
183
  async def acquire_channel(
154
184
  self,
155
185
  ) -> AsyncGenerator[aio_pika.abc.AbstractChannel, Any]:
156
- if not self.channel_pool:
157
- async with self.acquire_connection() as connection:
158
- yield await connection.channel(publisher_confirms=False)
159
- else:
160
- async with self.channel_pool.acquire() as channel:
161
- yield channel
186
+
187
+ async for attempt in tenacity.AsyncRetrying(
188
+ wait=tenacity.wait_exponential_jitter(initial=1, max=10),
189
+ before_sleep=tenacity.before_sleep_log(logger, logging.WARNING),
190
+ # stop=tenacity.stop_after_attempt(9000),
191
+ ):
192
+ with attempt:
193
+
194
+ if not self.connection_pool or not self.channel_pool:
195
+ async with self.acquire_connection() as connection:
196
+ yield await connection.channel(publisher_confirms=False)
197
+ else:
198
+
199
+ async with self.connection_pool.acquire() as connection:
200
+ if not connection.connected.is_set():
201
+ await connection.connect()
202
+
203
+ async with connection.channel(
204
+ publisher_confirms=False
205
+ ) as channel:
206
+ yield channel
207
+ # await connection.close()
208
+ if attempt.retry_state.attempt_number > 1:
209
+ logger.warning(
210
+ "Later successful connection attempt #%d",
211
+ attempt.retry_state.attempt_number,
212
+ )
162
213
 
163
214
  @asynccontextmanager
164
215
  async def provide_connection(self) -> AsyncGenerator[AIOPikaMessagePublisher, Any]:
@@ -0,0 +1,62 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ from dataclasses import dataclass
6
+ from datetime import UTC, datetime, timedelta, tzinfo
7
+
8
+ from jararaca.messagebus.publisher import IMessage, MessagePublisher
9
+
10
+
11
+ @dataclass
12
+ class CollectorDelayedMessageData:
13
+ message: IMessage
14
+ when: datetime
15
+ timezone: tzinfo
16
+
17
+
18
+ class MessagePublisherCollector(MessagePublisher):
19
+
20
+ def __init__(self) -> None:
21
+ self.staged_delayed_messages: list[CollectorDelayedMessageData] = []
22
+ self.staged_messages: list[IMessage] = []
23
+
24
+ async def publish(self, message: IMessage, topic: str) -> None:
25
+ self.staged_messages.append(message)
26
+
27
+ async def delay(self, message: IMessage, seconds: int) -> None:
28
+ self.staged_delayed_messages.append(
29
+ CollectorDelayedMessageData(
30
+ message=message,
31
+ when=datetime.now(UTC) + timedelta(seconds=seconds),
32
+ timezone=UTC,
33
+ )
34
+ )
35
+
36
+ async def schedule(
37
+ self, message: IMessage, when: datetime, timezone: tzinfo
38
+ ) -> None:
39
+ self.staged_delayed_messages.append(
40
+ CollectorDelayedMessageData(
41
+ message=message,
42
+ when=when,
43
+ timezone=timezone,
44
+ )
45
+ )
46
+
47
+ async def fill(self, publisher: MessagePublisher) -> None:
48
+ for message in self.staged_messages:
49
+ await publisher.publish(message, message.MESSAGE_TOPIC)
50
+
51
+ for delayed_message in self.staged_delayed_messages:
52
+ await publisher.schedule(
53
+ delayed_message.message,
54
+ delayed_message.when,
55
+ delayed_message.timezone,
56
+ )
57
+
58
+ def has_messages(self) -> bool:
59
+ return bool(self.staged_messages or self.staged_delayed_messages)
60
+
61
+ async def flush(self) -> None:
62
+ raise NotImplementedError("I'm just a poor little collector! :(")
@@ -1,7 +1,14 @@
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
 
4
8
  from jararaca.broker_backend import MessageBrokerBackend
9
+ from jararaca.messagebus.interceptors.message_publisher_collector import (
10
+ MessagePublisherCollector,
11
+ )
5
12
  from jararaca.messagebus.publisher import MessagePublisher, provide_message_publisher
6
13
  from jararaca.microservice import AppInterceptor, AppTransactionContext
7
14
 
@@ -18,10 +25,15 @@ class MessageBusPublisherInterceptor(AppInterceptor):
18
25
  connection_factory: MessageBusConnectionFactory,
19
26
  connection_name: str = "default",
20
27
  message_scheduler: MessageBrokerBackend | None = None,
28
+ *,
29
+ open_connection_at_end_of_transaction: bool = False,
21
30
  ):
22
31
  self.connection_factory = connection_factory
23
32
  self.connection_name = connection_name
24
33
  self.message_scheduler = message_scheduler
34
+ self.open_connection_at_end_of_transaction = (
35
+ open_connection_at_end_of_transaction
36
+ )
25
37
 
26
38
  @asynccontextmanager
27
39
  async def intercept(
@@ -31,8 +43,22 @@ class MessageBusPublisherInterceptor(AppInterceptor):
31
43
  yield
32
44
  return
33
45
 
34
- async with self.connection_factory.provide_connection() as connection:
35
- with provide_message_publisher(self.connection_name, connection):
46
+ if self.open_connection_at_end_of_transaction:
47
+
48
+ collector = MessagePublisherCollector()
49
+ with provide_message_publisher(self.connection_name, collector):
36
50
  yield
37
51
 
38
- await connection.flush()
52
+ if collector.has_messages():
53
+ async with self.connection_factory.provide_connection() as connection:
54
+ await collector.fill(connection)
55
+ await connection.flush()
56
+ return
57
+ else:
58
+
59
+ yield
60
+
61
+ async with self.connection_factory.provide_connection() as connection:
62
+ with provide_message_publisher(self.connection_name, connection):
63
+
64
+ await connection.flush()
@@ -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