jararaca 0.3.11a14__tar.gz → 0.3.11a16__tar.gz

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.

Potentially problematic release.


This version of jararaca might be problematic. Click here for more details.

Files changed (85) hide show
  1. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/PKG-INFO +1 -1
  2. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/docs/messagebus.md +2 -0
  3. jararaca-0.3.11a16/docs/retry.md +79 -0
  4. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/pyproject.toml +1 -1
  5. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/cli.py +280 -110
  6. jararaca-0.3.11a16/src/jararaca/messagebus/worker.py +1416 -0
  7. jararaca-0.3.11a16/src/jararaca/utils/retry.py +141 -0
  8. jararaca-0.3.11a14/src/jararaca/messagebus/worker.py +0 -644
  9. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/LICENSE +0 -0
  10. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/README.md +0 -0
  11. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/docs/CNAME +0 -0
  12. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/docs/architecture.md +0 -0
  13. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/docs/assets/_f04774c9-7e05-4da4-8b17-8be23f6a1475.jpeg +0 -0
  14. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/docs/assets/_f04774c9-7e05-4da4-8b17-8be23f6a1475.webp +0 -0
  15. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/docs/assets/tracing_example.png +0 -0
  16. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/docs/index.md +0 -0
  17. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/docs/scheduler.md +0 -0
  18. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/docs/stylesheets/custom.css +0 -0
  19. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/docs/websocket.md +0 -0
  20. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/__init__.py +0 -0
  21. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/__main__.py +0 -0
  22. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/broker_backend/__init__.py +0 -0
  23. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/broker_backend/mapper.py +0 -0
  24. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/broker_backend/redis_broker_backend.py +0 -0
  25. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/common/__init__.py +0 -0
  26. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/core/__init__.py +0 -0
  27. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/core/providers.py +0 -0
  28. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/core/uow.py +0 -0
  29. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/di.py +0 -0
  30. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/files/entity.py.mako +0 -0
  31. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/lifecycle.py +0 -0
  32. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/messagebus/__init__.py +0 -0
  33. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/messagebus/bus_message_controller.py +0 -0
  34. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/messagebus/consumers/__init__.py +0 -0
  35. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/messagebus/decorators.py +0 -0
  36. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/messagebus/interceptors/__init__.py +0 -0
  37. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +0 -0
  38. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/messagebus/interceptors/publisher_interceptor.py +0 -0
  39. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/messagebus/message.py +0 -0
  40. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/messagebus/publisher.py +0 -0
  41. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/microservice.py +0 -0
  42. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/observability/decorators.py +0 -0
  43. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/observability/interceptor.py +0 -0
  44. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/observability/providers/__init__.py +0 -0
  45. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/observability/providers/otel.py +0 -0
  46. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/persistence/base.py +0 -0
  47. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/persistence/exports.py +0 -0
  48. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/persistence/interceptors/__init__.py +0 -0
  49. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/persistence/interceptors/aiosqa_interceptor.py +0 -0
  50. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/persistence/session.py +0 -0
  51. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/persistence/sort_filter.py +0 -0
  52. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/persistence/utilities.py +0 -0
  53. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/presentation/__init__.py +0 -0
  54. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/presentation/decorators.py +0 -0
  55. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/presentation/hooks.py +0 -0
  56. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/presentation/http_microservice.py +0 -0
  57. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/presentation/server.py +0 -0
  58. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/presentation/websocket/__init__.py +0 -0
  59. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/presentation/websocket/base_types.py +0 -0
  60. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/presentation/websocket/context.py +0 -0
  61. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/presentation/websocket/decorators.py +0 -0
  62. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/presentation/websocket/redis.py +0 -0
  63. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/presentation/websocket/types.py +0 -0
  64. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/presentation/websocket/websocket_interceptor.py +0 -0
  65. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/py.typed +0 -0
  66. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/reflect/__init__.py +0 -0
  67. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/reflect/controller_inspect.py +0 -0
  68. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/reflect/metadata.py +0 -0
  69. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/rpc/__init__.py +0 -0
  70. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/rpc/http/__init__.py +0 -0
  71. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/rpc/http/backends/__init__.py +0 -0
  72. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/rpc/http/backends/httpx.py +0 -0
  73. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/rpc/http/backends/otel.py +0 -0
  74. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/rpc/http/decorators.py +0 -0
  75. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/rpc/http/httpx.py +0 -0
  76. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/scheduler/__init__.py +0 -0
  77. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/scheduler/beat_worker.py +0 -0
  78. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/scheduler/decorators.py +0 -0
  79. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/scheduler/types.py +0 -0
  80. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/tools/app_config/__init__.py +0 -0
  81. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/tools/app_config/decorators.py +0 -0
  82. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/tools/app_config/interceptor.py +0 -0
  83. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/tools/typescript/interface_parser.py +0 -0
  84. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/utils/__init__.py +0 -0
  85. {jararaca-0.3.11a14 → jararaca-0.3.11a16}/src/jararaca/utils/rabbitmq_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: jararaca
3
- Version: 0.3.11a14
3
+ Version: 0.3.11a16
4
4
  Summary: A simple and fast API framework for Python
5
5
  Author: Lucas S
6
6
  Author-email: me@luscasleo.dev
@@ -29,6 +29,7 @@ graph TB
29
29
  ack[ack]
30
30
  nack[nack]
31
31
  retry[retry]
32
+ retry_later[retry_later]
32
33
  reject[reject]
33
34
  end
34
35
 
@@ -42,6 +43,7 @@ graph TB
42
43
  BusMessageController --> nack
43
44
  BusMessageController --> reject
44
45
  BusMessageController --> retry
46
+ BusMessageController --> retry_later
45
47
  ```
46
48
 
47
49
  ## Message Structure
@@ -0,0 +1,79 @@
1
+ # Retry Mechanism with Exponential Backoff
2
+
3
+ Jararaca implements a robust retry mechanism with exponential backoff for handling transient failures in RabbitMQ connections and operations. This mechanism helps the system gracefully handle temporary network issues, broker unavailability, and other transient failures.
4
+
5
+ ## Core Components
6
+
7
+ The retry system consists of these main components:
8
+
9
+ 1. `RetryConfig` - Configuration class for customizing retry behavior
10
+ 2. `retry_with_backoff` - Utility function to execute operations with retry
11
+ 3. `with_retry` - Decorator for applying retry logic to functions
12
+
13
+ ## Retry Configuration
14
+
15
+ The `RetryConfig` class allows customization of various retry parameters:
16
+
17
+ ```python
18
+ class RetryConfig:
19
+ def __init__(
20
+ self,
21
+ max_retries: int = 5, # Maximum number of retry attempts
22
+ initial_delay: float = 1.0, # Initial delay between retries (seconds)
23
+ max_delay: float = 60.0, # Maximum delay between retries (seconds)
24
+ backoff_factor: float = 2.0, # Multiplier for delay after each retry
25
+ jitter: bool = True, # Add randomness to delay to prevent thundering herd
26
+ ):
27
+ ...
28
+ ```
29
+
30
+ ## Integration with MessageBus Worker
31
+
32
+ The RabbitMQ consumer in the message bus system uses the retry mechanism in several key areas:
33
+
34
+ 1. **Connection Establishment**: When establishing a connection to RabbitMQ, the system will automatically retry with increasing backoff periods if the connection fails.
35
+
36
+ 2. **Channel Creation**: When creating channels for publishing or consuming messages, failures trigger the retry mechanism.
37
+
38
+ 3. **Consumer Setup**: Setting up message consumers uses retry logic to handle temporary failures.
39
+
40
+ ## URL Configuration Parameters
41
+
42
+ Retry behavior can be customized through URL parameters when configuring the RabbitMQ connection:
43
+
44
+ | Parameter | Description | Default |
45
+ |-----------|-------------|---------|
46
+ | `connection_retry_max` | Maximum number of connection retry attempts | 5 |
47
+ | `connection_retry_delay` | Initial delay between connection retries (seconds) | 1.0 |
48
+ | `connection_retry_max_delay` | Maximum delay between connection retries (seconds) | 60.0 |
49
+ | `connection_retry_backoff` | Multiplier for delay after each connection retry | 2.0 |
50
+ | `consumer_retry_max` | Maximum number of consumer setup retry attempts | 3 |
51
+ | `consumer_retry_delay` | Initial delay between consumer setup retries (seconds) | 0.5 |
52
+ | `consumer_retry_max_delay` | Maximum delay between consumer setup retries (seconds) | 5.0 |
53
+ | `consumer_retry_backoff` | Multiplier for delay after each consumer setup retry | 2.0 |
54
+
55
+ ## Example Usage
56
+
57
+ ```python
58
+ # Configure with custom retry settings in URL:
59
+ broker_url = "amqp://guest:guest@localhost:5672/?exchange=jararaca&prefetch_count=10&connection_retry_max=10&connection_retry_delay=2.0"
60
+
61
+ # Use custom retry configuration in code:
62
+ from jararaca.utils.retry import RetryConfig, retry_with_backoff
63
+
64
+ config = RetryConfig(max_retries=3, initial_delay=1.0, max_delay=30.0)
65
+
66
+ async def connect_with_retry():
67
+ return await retry_with_backoff(
68
+ establish_connection,
69
+ retry_config=config,
70
+ retry_exceptions=(ConnectionError, TimeoutError)
71
+ )
72
+ ```
73
+
74
+ ## Benefits
75
+
76
+ 1. **Resilience** - The system can recover automatically from transient failures
77
+ 2. **Reduced downtime** - Automatic reconnection minimizes service disruption
78
+ 3. **Configuration flexibility** - Retry behavior can be tailored to different environments
79
+ 4. **Smart backoff** - Exponential backoff with jitter prevents overloading services during recovery
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "jararaca"
3
- version = "0.3.11a14"
3
+ version = "0.3.11a16"
4
4
  description = "A simple and fast API framework for Python"
5
5
  authors = ["Lucas S <me@luscasleo.dev>"]
6
6
  readme = "README.md"
@@ -20,7 +20,10 @@ from jararaca.messagebus.decorators import MessageBusController, MessageHandler
20
20
  from jararaca.microservice import Microservice
21
21
  from jararaca.presentation.http_microservice import HttpMicroservice
22
22
  from jararaca.presentation.server import create_http_server
23
- from jararaca.reflect.controller_inspect import inspect_controller
23
+ from jararaca.reflect.controller_inspect import (
24
+ ControllerMemberReflect,
25
+ inspect_controller,
26
+ )
24
27
  from jararaca.scheduler.beat_worker import BeatWorker
25
28
  from jararaca.scheduler.decorators import ScheduledAction
26
29
  from jararaca.tools.typescript.interface_parser import (
@@ -83,7 +86,6 @@ async def declare_worker_infrastructure(
83
86
  """
84
87
  Declare the infrastructure (exchanges and queues) for worker.
85
88
  """
86
-
87
89
  parsed_url = urlparse(broker_url)
88
90
  if parsed_url.scheme not in ["amqp", "amqps"]:
89
91
  raise ValueError(f"Unsupported broker URL scheme: {parsed_url.scheme}")
@@ -98,41 +100,161 @@ async def declare_worker_infrastructure(
98
100
 
99
101
  exchange = query_params["exchange"][0]
100
102
 
103
+ # Create a connection that will be used to create channels for each operation
101
104
  connection = await aio_pika.connect(broker_url)
102
- channel = await connection.channel()
103
105
 
104
- # Only force delete infrastructure if requested at the beginning
105
- if force or (
106
- interactive_mode
107
- and click.confirm(f"Delete existing infrastructure for exchange: {exchange}?")
106
+ try:
107
+ # Step 1: Setup infrastructure (exchanges and dead letter queues)
108
+ # Creating a dedicated channel for infrastructure setup
109
+ await setup_infrastructure(connection, exchange, force, interactive_mode)
110
+
111
+ # Step 2: Declare all message handlers and scheduled actions queues
112
+ # Creating a dedicated channel for controller queues
113
+ await declare_controller_queues(
114
+ connection, app, exchange, force, interactive_mode
115
+ )
116
+ finally:
117
+ # Always close the connection in the finally block
118
+ await connection.close()
119
+
120
+
121
+ async def setup_infrastructure(
122
+ connection: aio_pika.abc.AbstractConnection,
123
+ exchange: str,
124
+ force: bool = False,
125
+ interactive_mode: bool = False,
126
+ ) -> None:
127
+ """
128
+ Setup the basic infrastructure (exchanges and dead letter queues).
129
+ """
130
+ # Check if infrastructure exists
131
+ infrastructure_exists = await check_infrastructure_exists(connection, exchange)
132
+
133
+ # If it exists and force or user confirms, delete it
134
+ if not infrastructure_exists and should_recreate_infrastructure(
135
+ force,
136
+ interactive_mode,
137
+ f"Existing infrastructure found for exchange '{exchange}'. Delete and recreate?",
108
138
  ):
139
+ await delete_infrastructure(connection, exchange)
140
+
141
+ # Try to declare required infrastructure
142
+ await declare_infrastructure(connection, exchange, force, interactive_mode)
143
+
144
+
145
+ async def check_infrastructure_exists(
146
+ connection: aio_pika.abc.AbstractConnection, exchange: str
147
+ ) -> bool:
148
+ """
149
+ Check if the infrastructure exists by trying passive declarations.
150
+ """
151
+ # Create a dedicated channel for checking infrastructure existence using async context manager
152
+ async with connection.channel() as channel:
153
+ try:
154
+ await RabbitmqUtils.declare_main_exchange(
155
+ channel=channel,
156
+ exchange_name=exchange,
157
+ passive=True,
158
+ )
159
+ await RabbitmqUtils.declare_dl_exchange(channel=channel, passive=True)
160
+ await RabbitmqUtils.declare_dl_queue(channel=channel, passive=True)
161
+ return True
162
+ except Exception:
163
+ # Infrastructure doesn't exist, which is fine for fresh setup
164
+ return False
165
+
166
+
167
+ def should_recreate_infrastructure(
168
+ force: bool, interactive_mode: bool, confirmation_message: str
169
+ ) -> bool:
170
+ """
171
+ Determine if infrastructure should be recreated based on force flag and user input.
172
+ """
173
+ return force or (interactive_mode and click.confirm(confirmation_message))
174
+
175
+
176
+ async def delete_infrastructure(
177
+ connection: aio_pika.abc.AbstractConnection, exchange: str
178
+ ) -> None:
179
+ """
180
+ Delete existing infrastructure (exchanges and queues).
181
+ """
182
+ # Create a dedicated channel for deleting infrastructure using async context manager
183
+ async with connection.channel() as channel:
109
184
  click.echo(f"→ Deleting existing infrastructure for exchange: {exchange}")
110
185
  await RabbitmqUtils.delete_exchange(channel, exchange)
111
186
  await RabbitmqUtils.delete_exchange(channel, RabbitmqUtils.DEAD_LETTER_EXCHANGE)
112
187
  await RabbitmqUtils.delete_queue(channel, RabbitmqUtils.DEAD_LETTER_QUEUE)
113
188
 
114
- try:
115
- await RabbitmqUtils.declare_main_exchange(
116
- channel=channel,
117
- exchange_name=exchange,
118
- passive=False,
119
- )
120
189
 
121
- dlx = await RabbitmqUtils.declare_dl_exchange(channel=channel, passive=False)
122
- dlq = await RabbitmqUtils.declare_dl_queue(channel=channel, passive=False)
123
- await dlq.bind(dlx, routing_key=RabbitmqUtils.DEAD_LETTER_EXCHANGE)
124
- except Exception as e:
125
- click.echo(f"Error during exchange declaration: {e}")
126
- if force or (
127
- interactive_mode
128
- and click.confirm("Error occurred. Recreate infrastructure?")
129
- ):
130
- await channel.close()
131
- await connection.close()
132
- raise
133
- click.echo("Skipping main exchange declaration due to error")
190
+ async def declare_infrastructure(
191
+ connection: aio_pika.abc.AbstractConnection,
192
+ exchange: str,
193
+ force: bool,
194
+ interactive_mode: bool,
195
+ ) -> None:
196
+ """
197
+ Declare the required infrastructure (exchanges and dead letter queues).
198
+ """
199
+ # Using async context manager for channel creation
200
+ async with connection.channel() as channel:
201
+ try:
202
+ # Declare main exchange
203
+ await RabbitmqUtils.declare_main_exchange(
204
+ channel=channel,
205
+ exchange_name=exchange,
206
+ passive=False,
207
+ )
134
208
 
135
- # Find all message handlers and scheduled actions
209
+ # Declare dead letter exchange and queue
210
+ dlx = await RabbitmqUtils.declare_dl_exchange(
211
+ channel=channel, passive=False
212
+ )
213
+ dlq = await RabbitmqUtils.declare_dl_queue(channel=channel, passive=False)
214
+ await dlq.bind(dlx, routing_key=RabbitmqUtils.DEAD_LETTER_EXCHANGE)
215
+
216
+ except Exception as e:
217
+ click.echo(f"Error during exchange declaration: {e}")
218
+
219
+ # If interactive mode and user confirms, or if forced, try again after deletion
220
+ if should_recreate_infrastructure(
221
+ force, interactive_mode, "Delete existing infrastructure and recreate?"
222
+ ):
223
+ # Delete infrastructure with a new channel
224
+ await delete_infrastructure(connection, exchange)
225
+
226
+ # Try again with a new channel
227
+ async with connection.channel() as new_channel:
228
+ # Try again after deletion
229
+ await RabbitmqUtils.declare_main_exchange(
230
+ channel=new_channel,
231
+ exchange_name=exchange,
232
+ passive=False,
233
+ )
234
+ dlx = await RabbitmqUtils.declare_dl_exchange(
235
+ channel=new_channel, passive=False
236
+ )
237
+ dlq = await RabbitmqUtils.declare_dl_queue(
238
+ channel=new_channel, passive=False
239
+ )
240
+ await dlq.bind(dlx, routing_key=RabbitmqUtils.DEAD_LETTER_EXCHANGE)
241
+ elif force:
242
+ # If force is true but recreation failed, propagate the error
243
+ raise
244
+ else:
245
+ click.echo("Skipping main exchange declaration due to error")
246
+
247
+
248
+ async def declare_controller_queues(
249
+ connection: aio_pika.abc.AbstractConnection,
250
+ app: Microservice,
251
+ exchange: str,
252
+ force: bool,
253
+ interactive_mode: bool,
254
+ ) -> None:
255
+ """
256
+ Declare all message handler and scheduled action queues for controllers.
257
+ """
136
258
  for instance_type in app.controllers:
137
259
  controller_spec = MessageBusController.get_messagebus(instance_type)
138
260
  if controller_spec is None:
@@ -140,54 +262,135 @@ async def declare_worker_infrastructure(
140
262
 
141
263
  _, members = inspect_controller(instance_type)
142
264
 
143
- # Declare queues for message handlers
265
+ # Process each member (method) in the controller
144
266
  for _, member in members.items():
145
- message_handler = MessageHandler.get_message_incoming(
146
- member.member_function
267
+ # Check if it's a message handler
268
+ await declare_message_handler_queue(
269
+ connection, member, exchange, force, interactive_mode
147
270
  )
148
- if message_handler is not None:
149
- queue_name = f"{message_handler.message_type.MESSAGE_TOPIC}.{member.member_function.__module__}.{member.member_function.__qualname__}"
150
- routing_key = f"{message_handler.message_type.MESSAGE_TOPIC}.#"
151
-
152
- try:
153
- # Try to declare queue
154
- queue = await RabbitmqUtils.declare_worker_queue(
155
- channel=channel, queue_name=queue_name, passive=False
156
- )
157
- await queue.bind(exchange=exchange, routing_key=routing_key)
158
- click.echo(
159
- f"✓ Declared message handler queue: {queue_name} (routing key: {routing_key})"
160
- )
161
- except Exception as e:
162
- click.echo(
163
- f"⚠ Skipping message handler queue {queue_name} due to error: {e}"
164
- )
165
- continue
166
271
 
167
- scheduled_action = ScheduledAction.get_scheduled_action(
168
- member.member_function
272
+ # Check if it's a scheduled action
273
+ await declare_scheduled_action_queue(
274
+ connection, member, exchange, force, interactive_mode
169
275
  )
170
- if scheduled_action is not None:
171
- queue_name = f"{member.member_function.__module__}.{member.member_function.__qualname__}"
172
- routing_key = queue_name
173
-
174
- try:
175
- # Try to declare queue
176
- queue = await RabbitmqUtils.declare_scheduled_action_queue(
177
- channel=channel, queue_name=queue_name, passive=False
178
- )
276
+
277
+
278
+ async def declare_message_handler_queue(
279
+ connection: aio_pika.abc.AbstractConnection,
280
+ member: ControllerMemberReflect,
281
+ exchange: str,
282
+ force: bool,
283
+ interactive_mode: bool,
284
+ ) -> None:
285
+ """
286
+ Declare a queue for a message handler if the member is one.
287
+ """
288
+ message_handler = MessageHandler.get_message_incoming(member.member_function)
289
+ if message_handler is not None:
290
+ queue_name = f"{message_handler.message_type.MESSAGE_TOPIC}.{member.member_function.__module__}.{member.member_function.__qualname__}"
291
+ routing_key = f"{message_handler.message_type.MESSAGE_TOPIC}.#"
292
+
293
+ await declare_and_bind_queue(
294
+ connection,
295
+ queue_name,
296
+ routing_key,
297
+ exchange,
298
+ force,
299
+ interactive_mode,
300
+ is_scheduled_action=False,
301
+ )
302
+
303
+
304
+ async def declare_scheduled_action_queue(
305
+ connection: aio_pika.abc.AbstractConnection,
306
+ member: ControllerMemberReflect,
307
+ exchange: str,
308
+ force: bool,
309
+ interactive_mode: bool,
310
+ ) -> None:
311
+ """
312
+ Declare a queue for a scheduled action if the member is one.
313
+ """
314
+ scheduled_action = ScheduledAction.get_scheduled_action(member.member_function)
315
+ if scheduled_action is not None:
316
+ queue_name = (
317
+ f"{member.member_function.__module__}.{member.member_function.__qualname__}"
318
+ )
319
+ routing_key = queue_name
320
+
321
+ await declare_and_bind_queue(
322
+ connection,
323
+ queue_name,
324
+ routing_key,
325
+ exchange,
326
+ force,
327
+ interactive_mode,
328
+ is_scheduled_action=True,
329
+ )
330
+
331
+
332
+ async def declare_and_bind_queue(
333
+ connection: aio_pika.abc.AbstractConnection,
334
+ queue_name: str,
335
+ routing_key: str,
336
+ exchange: str,
337
+ force: bool,
338
+ interactive_mode: bool,
339
+ is_scheduled_action: bool,
340
+ ) -> None:
341
+ """
342
+ Declare and bind a queue to the exchange with the given routing key.
343
+ """
344
+ queue_type = "scheduled action" if is_scheduled_action else "message handler"
345
+
346
+ # Using async context manager for channel creation
347
+ async with connection.channel() as channel:
348
+ try:
349
+ # Try to declare queue using the appropriate method
350
+ if is_scheduled_action:
351
+ queue = await RabbitmqUtils.declare_scheduled_action_queue(
352
+ channel=channel, queue_name=queue_name, passive=False
353
+ )
354
+ else:
355
+ queue = await RabbitmqUtils.declare_worker_queue(
356
+ channel=channel, queue_name=queue_name, passive=False
357
+ )
358
+
359
+ # Bind the queue to the exchange
360
+ await queue.bind(exchange=exchange, routing_key=routing_key)
361
+ click.echo(
362
+ f"✓ Declared {queue_type} queue: {queue_name} (routing key: {routing_key})"
363
+ )
364
+ except Exception as e:
365
+ click.echo(f"⚠ Error declaring {queue_type} queue {queue_name}: {e}")
366
+
367
+ # If interactive mode and user confirms, or if forced, recreate the queue
368
+ if force or (
369
+ interactive_mode
370
+ and click.confirm(f"Delete and recreate queue {queue_name}?")
371
+ ):
372
+ # Create a new channel for deletion and recreation
373
+ async with connection.channel() as new_channel:
374
+ # Delete the queue
375
+ await RabbitmqUtils.delete_queue(new_channel, queue_name)
376
+
377
+ # Try to declare queue again using the appropriate method
378
+ if is_scheduled_action:
379
+ queue = await RabbitmqUtils.declare_scheduled_action_queue(
380
+ channel=new_channel, queue_name=queue_name, passive=False
381
+ )
382
+ else:
383
+ queue = await RabbitmqUtils.declare_worker_queue(
384
+ channel=new_channel, queue_name=queue_name, passive=False
385
+ )
386
+
387
+ # Bind the queue to the exchange
179
388
  await queue.bind(exchange=exchange, routing_key=routing_key)
180
389
  click.echo(
181
- f"✓ Declared scheduled action queue: {queue_name} (routing key: {routing_key})"
182
- )
183
- except Exception as e:
184
- click.echo(
185
- f"⚠ Skipping scheduled action queue {queue_name} due to error: {e}"
390
+ f"✓ Recreated {queue_type} queue: {queue_name} (routing key: {routing_key})"
186
391
  )
187
- continue
188
-
189
- await channel.close()
190
- await connection.close()
392
+ else:
393
+ click.echo(f"⚠ Skipping {queue_type} queue {queue_name}")
191
394
 
192
395
 
193
396
  @click.group()
@@ -562,29 +765,14 @@ def gen_entity(entity_name: str, file_path: StreamWriter) -> None:
562
765
  help="Broker URL (e.g., amqp://guest:guest@localhost/) [env: BROKER_URL]",
563
766
  )
564
767
  @click.option(
565
- "--exchange",
566
- type=str,
567
- default="jararaca_ex",
568
- envvar="EXCHANGE",
569
- help="Exchange name [env: EXCHANGE]",
570
- )
571
- @click.option(
572
- "--passive-declare",
768
+ "-i",
769
+ "--interactive-mode",
573
770
  is_flag=True,
574
771
  default=False,
575
- help="Use passive declarations (check if infrastructure exists without creating it)",
576
- )
577
- @click.option(
578
- "--handlers",
579
- type=str,
580
- help="Comma-separated list of handler names to declare queues for. If not specified, all handlers will be declared.",
581
- )
582
- @click.option(
583
- "--schedulers",
584
- type=str,
585
- help="Comma-separated list of scheduler names to declare queues for. If not specified, all schedulers will be declared.",
772
+ help="Enable interactive mode for queue declaration (confirm before deleting existing queues)",
586
773
  )
587
774
  @click.option(
775
+ "-f",
588
776
  "--force",
589
777
  is_flag=True,
590
778
  default=False,
@@ -592,12 +780,9 @@ def gen_entity(entity_name: str, file_path: StreamWriter) -> None:
592
780
  )
593
781
  def declare(
594
782
  app_path: str,
595
- broker_url: str | None,
596
- exchange: str,
597
- passive_declare: bool,
598
- handlers: str | None,
599
- schedulers: str | None,
783
+ broker_url: str,
600
784
  force: bool,
785
+ interactive_mode: bool,
601
786
  ) -> None:
602
787
  """
603
788
  Declare RabbitMQ infrastructure (exchanges and queues) for message handlers and schedulers.
@@ -636,28 +821,13 @@ def declare(
636
821
  )
637
822
  return
638
823
 
639
- # Parse handler names if provided
640
- handler_names: set[str] | None = None
641
- if handlers:
642
- handler_names = {
643
- name.strip() for name in handlers.split(",") if name.strip()
644
- }
645
-
646
- # Parse scheduler names if provided
647
- scheduler_names: set[str] | None = None
648
- if schedulers:
649
- scheduler_names = {
650
- name.strip() for name in schedulers.split(",") if name.strip()
651
- }
652
-
653
824
  try:
654
825
  # Create the broker URL with exchange parameter
655
- broker_url_with_exchange = f"{broker_url}?exchange={exchange}"
656
826
 
657
- click.echo(
658
- f"→ Declaring worker infrastructure (URL: {broker_url_with_exchange})"
827
+ click.echo(f"→ Declaring worker infrastructure (URL: {broker_url})")
828
+ await declare_worker_infrastructure(
829
+ broker_url, app, force, interactive_mode
659
830
  )
660
- await declare_worker_infrastructure(broker_url_with_exchange, app, force)
661
831
 
662
832
  click.echo("✓ Workers infrastructure declared successfully!")
663
833
  except Exception as e: