jararaca 0.3.10__py3-none-any.whl → 0.3.11__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.
Potentially problematic release.
This version of jararaca might be problematic. Click here for more details.
- jararaca/__init__.py +76 -5
- jararaca/cli.py +460 -116
- jararaca/core/uow.py +17 -12
- jararaca/messagebus/decorators.py +33 -30
- jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +30 -2
- jararaca/messagebus/interceptors/publisher_interceptor.py +7 -3
- jararaca/messagebus/publisher.py +14 -6
- jararaca/messagebus/worker.py +1102 -88
- jararaca/microservice.py +137 -34
- jararaca/observability/decorators.py +7 -3
- jararaca/observability/interceptor.py +4 -2
- jararaca/observability/providers/otel.py +14 -10
- jararaca/persistence/base.py +2 -1
- jararaca/persistence/interceptors/aiosqa_interceptor.py +167 -16
- jararaca/presentation/decorators.py +96 -10
- jararaca/presentation/server.py +31 -4
- jararaca/presentation/websocket/context.py +30 -4
- jararaca/presentation/websocket/types.py +2 -2
- jararaca/presentation/websocket/websocket_interceptor.py +28 -4
- jararaca/reflect/__init__.py +0 -0
- jararaca/reflect/controller_inspect.py +75 -0
- jararaca/{tools → reflect}/metadata.py +25 -5
- jararaca/scheduler/{scheduler_v2.py → beat_worker.py} +49 -53
- jararaca/scheduler/decorators.py +55 -20
- jararaca/tools/app_config/interceptor.py +4 -2
- jararaca/utils/rabbitmq_utils.py +259 -5
- jararaca/utils/retry.py +141 -0
- {jararaca-0.3.10.dist-info → jararaca-0.3.11.dist-info}/METADATA +2 -1
- {jararaca-0.3.10.dist-info → jararaca-0.3.11.dist-info}/RECORD +32 -31
- {jararaca-0.3.10.dist-info → jararaca-0.3.11.dist-info}/WHEEL +1 -1
- jararaca/messagebus/worker_v2.py +0 -617
- jararaca/scheduler/scheduler.py +0 -161
- {jararaca-0.3.10.dist-info → jararaca-0.3.11.dist-info}/LICENSE +0 -0
- {jararaca-0.3.10.dist-info → jararaca-0.3.11.dist-info}/entry_points.txt +0 -0
jararaca/cli.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import importlib
|
|
2
3
|
import importlib.resources
|
|
3
4
|
import multiprocessing
|
|
@@ -7,22 +8,28 @@ import time
|
|
|
7
8
|
from codecs import StreamWriter
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import Any
|
|
10
|
-
from urllib.parse import
|
|
11
|
+
from urllib.parse import parse_qs, urlparse
|
|
11
12
|
|
|
13
|
+
import aio_pika
|
|
12
14
|
import click
|
|
13
15
|
import uvicorn
|
|
14
|
-
from mako.template import Template
|
|
16
|
+
from mako.template import Template
|
|
15
17
|
|
|
16
|
-
from jararaca.messagebus import worker as
|
|
17
|
-
from jararaca.messagebus import
|
|
18
|
+
from jararaca.messagebus import worker as worker_mod
|
|
19
|
+
from jararaca.messagebus.decorators import MessageBusController, MessageHandler
|
|
18
20
|
from jararaca.microservice import Microservice
|
|
19
21
|
from jararaca.presentation.http_microservice import HttpMicroservice
|
|
20
22
|
from jararaca.presentation.server import create_http_server
|
|
21
|
-
from jararaca.
|
|
22
|
-
|
|
23
|
+
from jararaca.reflect.controller_inspect import (
|
|
24
|
+
ControllerMemberReflect,
|
|
25
|
+
inspect_controller,
|
|
26
|
+
)
|
|
27
|
+
from jararaca.scheduler.beat_worker import BeatWorker
|
|
28
|
+
from jararaca.scheduler.decorators import ScheduledAction
|
|
23
29
|
from jararaca.tools.typescript.interface_parser import (
|
|
24
30
|
write_microservice_to_typescript_interface,
|
|
25
31
|
)
|
|
32
|
+
from jararaca.utils.rabbitmq_utils import RabbitmqUtils
|
|
26
33
|
|
|
27
34
|
LIBRARY_FILES_PATH = importlib.resources.files("jararaca.files")
|
|
28
35
|
ENTITY_TEMPLATE_PATH = LIBRARY_FILES_PATH / "entity.py.mako"
|
|
@@ -67,89 +74,328 @@ def find_microservice_by_module_path(module_path: str) -> Microservice:
|
|
|
67
74
|
return app
|
|
68
75
|
|
|
69
76
|
|
|
70
|
-
|
|
71
|
-
def cli() -> None:
|
|
72
|
-
pass
|
|
77
|
+
# The v1 infrastructure declaration function has been removed as part of the CLI simplification
|
|
73
78
|
|
|
74
79
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
"
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
"
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
80
|
+
async def declare_worker_infrastructure(
|
|
81
|
+
broker_url: str,
|
|
82
|
+
app: Microservice,
|
|
83
|
+
force: bool = False,
|
|
84
|
+
interactive_mode: bool = False,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""
|
|
87
|
+
Declare the infrastructure (exchanges and queues) for worker.
|
|
88
|
+
"""
|
|
89
|
+
parsed_url = urlparse(broker_url)
|
|
90
|
+
if parsed_url.scheme not in ["amqp", "amqps"]:
|
|
91
|
+
raise ValueError(f"Unsupported broker URL scheme: {parsed_url.scheme}")
|
|
92
|
+
|
|
93
|
+
if not parsed_url.query:
|
|
94
|
+
raise ValueError("Query string must be set for AMQP URLs")
|
|
95
|
+
|
|
96
|
+
query_params = parse_qs(parsed_url.query)
|
|
97
|
+
|
|
98
|
+
if "exchange" not in query_params or not query_params["exchange"]:
|
|
99
|
+
raise ValueError("Exchange must be set in the query string")
|
|
100
|
+
|
|
101
|
+
exchange = query_params["exchange"][0]
|
|
102
|
+
|
|
103
|
+
# Create a connection that will be used to create channels for each operation
|
|
104
|
+
connection = await aio_pika.connect(broker_url)
|
|
105
|
+
|
|
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,
|
|
115
123
|
exchange: str,
|
|
116
|
-
|
|
117
|
-
|
|
124
|
+
force: bool = False,
|
|
125
|
+
interactive_mode: bool = False,
|
|
118
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?",
|
|
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
|
|
119
165
|
|
|
120
|
-
app = find_microservice_by_module_path(app_path)
|
|
121
166
|
|
|
122
|
-
|
|
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))
|
|
123
174
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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:
|
|
184
|
+
click.echo(f"→ Deleting existing infrastructure for exchange: {exchange}")
|
|
185
|
+
await RabbitmqUtils.delete_exchange(channel, exchange)
|
|
186
|
+
await RabbitmqUtils.delete_exchange(channel, RabbitmqUtils.DEAD_LETTER_EXCHANGE)
|
|
187
|
+
await RabbitmqUtils.delete_queue(channel, RabbitmqUtils.DEAD_LETTER_QUEUE)
|
|
188
|
+
|
|
189
|
+
|
|
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,
|
|
130
207
|
)
|
|
131
|
-
)
|
|
132
208
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
+
"""
|
|
258
|
+
for instance_type in app.controllers:
|
|
259
|
+
controller_spec = MessageBusController.get_messagebus(instance_type)
|
|
260
|
+
if controller_spec is None:
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
_, members = inspect_controller(instance_type)
|
|
264
|
+
|
|
265
|
+
# Process each member (method) in the controller
|
|
266
|
+
for _, member in members.items():
|
|
267
|
+
# Check if it's a message handler
|
|
268
|
+
await declare_message_handler_queue(
|
|
269
|
+
connection, member, exchange, force, interactive_mode
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Check if it's a scheduled action
|
|
273
|
+
await declare_scheduled_action_queue(
|
|
274
|
+
connection, member, exchange, force, interactive_mode
|
|
139
275
|
)
|
|
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,
|
|
140
301
|
)
|
|
141
302
|
|
|
142
|
-
url = parsed_url.geturl()
|
|
143
303
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
388
|
+
await queue.bind(exchange=exchange, routing_key=routing_key)
|
|
389
|
+
click.echo(
|
|
390
|
+
f"✓ Recreated {queue_type} queue: {queue_name} (routing key: {routing_key})"
|
|
391
|
+
)
|
|
392
|
+
else:
|
|
393
|
+
click.echo(f"⚠ Skipping {queue_type} queue {queue_name}")
|
|
149
394
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
395
|
+
|
|
396
|
+
@click.group()
|
|
397
|
+
def cli() -> None:
|
|
398
|
+
pass
|
|
153
399
|
|
|
154
400
|
|
|
155
401
|
@cli.command()
|
|
@@ -162,20 +408,37 @@ def worker(
|
|
|
162
408
|
"--broker-url",
|
|
163
409
|
type=str,
|
|
164
410
|
envvar="BROKER_URL",
|
|
411
|
+
required=True,
|
|
412
|
+
help="The URL for the message broker",
|
|
165
413
|
)
|
|
166
414
|
@click.option(
|
|
167
415
|
"--backend-url",
|
|
168
416
|
type=str,
|
|
169
417
|
envvar="BACKEND_URL",
|
|
418
|
+
required=True,
|
|
419
|
+
help="The URL for the message broker backend",
|
|
170
420
|
)
|
|
171
|
-
|
|
172
|
-
|
|
421
|
+
@click.option(
|
|
422
|
+
"--handlers",
|
|
423
|
+
type=str,
|
|
424
|
+
help="Comma-separated list of handler names to listen to. If not specified, all handlers will be used.",
|
|
425
|
+
)
|
|
426
|
+
def worker(
|
|
427
|
+
app_path: str, broker_url: str, backend_url: str, handlers: str | None
|
|
428
|
+
) -> None:
|
|
429
|
+
"""Start a message bus worker that processes asynchronous messages from a message queue."""
|
|
173
430
|
app = find_microservice_by_module_path(app_path)
|
|
174
431
|
|
|
175
|
-
|
|
432
|
+
# Parse handler names if provided
|
|
433
|
+
handler_names: set[str] | None = None
|
|
434
|
+
if handlers:
|
|
435
|
+
handler_names = {name.strip() for name in handlers.split(",") if name.strip()}
|
|
436
|
+
|
|
437
|
+
worker_mod.MessageBusWorker(
|
|
176
438
|
app=app,
|
|
177
439
|
broker_url=broker_url,
|
|
178
440
|
backend_url=backend_url,
|
|
441
|
+
handler_names=handler_names,
|
|
179
442
|
).start_sync()
|
|
180
443
|
|
|
181
444
|
|
|
@@ -218,25 +481,6 @@ def server(app_path: str, host: str, port: int) -> None:
|
|
|
218
481
|
uvicorn.run(asgi_app, host=host, port=port)
|
|
219
482
|
|
|
220
483
|
|
|
221
|
-
@cli.command()
|
|
222
|
-
@click.argument(
|
|
223
|
-
"app_path",
|
|
224
|
-
type=str,
|
|
225
|
-
)
|
|
226
|
-
@click.option(
|
|
227
|
-
"--interval",
|
|
228
|
-
type=int,
|
|
229
|
-
default=1,
|
|
230
|
-
)
|
|
231
|
-
def scheduler(
|
|
232
|
-
app_path: str,
|
|
233
|
-
interval: int,
|
|
234
|
-
) -> None:
|
|
235
|
-
app = find_microservice_by_module_path(app_path)
|
|
236
|
-
|
|
237
|
-
Scheduler(app, interval=interval).run()
|
|
238
|
-
|
|
239
|
-
|
|
240
484
|
@cli.command()
|
|
241
485
|
@click.argument(
|
|
242
486
|
"app_path",
|
|
@@ -258,21 +502,34 @@ def scheduler(
|
|
|
258
502
|
type=str,
|
|
259
503
|
required=True,
|
|
260
504
|
)
|
|
261
|
-
|
|
505
|
+
@click.option(
|
|
506
|
+
"--actions",
|
|
507
|
+
type=str,
|
|
508
|
+
help="Comma-separated list of action names to run (only run actions with these names)",
|
|
509
|
+
)
|
|
510
|
+
def beat(
|
|
262
511
|
interval: int,
|
|
263
512
|
broker_url: str,
|
|
264
513
|
backend_url: str,
|
|
265
514
|
app_path: str,
|
|
515
|
+
actions: str | None = None,
|
|
266
516
|
) -> None:
|
|
267
517
|
|
|
268
518
|
app = find_microservice_by_module_path(app_path)
|
|
269
|
-
|
|
519
|
+
|
|
520
|
+
# Parse scheduler names if provided
|
|
521
|
+
scheduler_names: set[str] | None = None
|
|
522
|
+
if actions:
|
|
523
|
+
scheduler_names = {name.strip() for name in actions.split(",") if name.strip()}
|
|
524
|
+
|
|
525
|
+
beat_worker = BeatWorker(
|
|
270
526
|
app=app,
|
|
271
527
|
interval=interval,
|
|
272
528
|
backend_url=backend_url,
|
|
273
529
|
broker_url=broker_url,
|
|
530
|
+
scheduled_action_names=scheduler_names,
|
|
274
531
|
)
|
|
275
|
-
|
|
532
|
+
beat_worker.run()
|
|
276
533
|
|
|
277
534
|
|
|
278
535
|
def generate_interfaces(
|
|
@@ -303,7 +560,7 @@ def generate_interfaces(
|
|
|
303
560
|
file.write(content)
|
|
304
561
|
file.flush()
|
|
305
562
|
|
|
306
|
-
|
|
563
|
+
click.echo(
|
|
307
564
|
f"Generated TypeScript interfaces at {time.strftime('%H:%M:%S')} at {str(Path(file_path).absolute())}"
|
|
308
565
|
)
|
|
309
566
|
|
|
@@ -311,19 +568,19 @@ def generate_interfaces(
|
|
|
311
568
|
import subprocess
|
|
312
569
|
|
|
313
570
|
try:
|
|
314
|
-
|
|
571
|
+
click.echo(f"Running post-process command: {post_process_cmd}")
|
|
315
572
|
subprocess.run(
|
|
316
573
|
post_process_cmd.replace("{file}", file_path),
|
|
317
574
|
shell=True,
|
|
318
575
|
check=True,
|
|
319
576
|
)
|
|
320
|
-
|
|
577
|
+
click.echo(f"Post-processing completed successfully")
|
|
321
578
|
except subprocess.CalledProcessError as e:
|
|
322
|
-
|
|
579
|
+
click.echo(f"Post-processing command failed: {e}", file=sys.stderr)
|
|
323
580
|
|
|
324
581
|
return content
|
|
325
582
|
except Exception as e:
|
|
326
|
-
|
|
583
|
+
click.echo(f"Error generating TypeScript interfaces: {e}", file=sys.stderr)
|
|
327
584
|
return ""
|
|
328
585
|
|
|
329
586
|
|
|
@@ -367,18 +624,20 @@ def gen_tsi(
|
|
|
367
624
|
"""Generate TypeScript interfaces from a Python microservice."""
|
|
368
625
|
|
|
369
626
|
if stdout and watch:
|
|
370
|
-
|
|
627
|
+
click.echo(
|
|
371
628
|
"Error: --watch and --stdout options cannot be used together",
|
|
372
629
|
file=sys.stderr,
|
|
373
630
|
)
|
|
374
631
|
return
|
|
375
632
|
|
|
376
633
|
if not file_path and not stdout:
|
|
377
|
-
|
|
634
|
+
click.echo(
|
|
635
|
+
"Error: either file_path or --stdout must be provided", file=sys.stderr
|
|
636
|
+
)
|
|
378
637
|
return
|
|
379
638
|
|
|
380
639
|
if post_process and stdout:
|
|
381
|
-
|
|
640
|
+
click.echo(
|
|
382
641
|
"Error: --post-process and --stdout options cannot be used together",
|
|
383
642
|
file=sys.stderr,
|
|
384
643
|
)
|
|
@@ -388,7 +647,7 @@ def gen_tsi(
|
|
|
388
647
|
content = generate_interfaces(app_path, file_path, stdout, post_process)
|
|
389
648
|
|
|
390
649
|
if stdout:
|
|
391
|
-
|
|
650
|
+
click.echo(content)
|
|
392
651
|
return
|
|
393
652
|
|
|
394
653
|
# If watch mode is not enabled, exit
|
|
@@ -399,7 +658,7 @@ def gen_tsi(
|
|
|
399
658
|
from watchdog.events import FileSystemEvent, FileSystemEventHandler
|
|
400
659
|
from watchdog.observers import Observer
|
|
401
660
|
except ImportError:
|
|
402
|
-
|
|
661
|
+
click.echo(
|
|
403
662
|
"Watchdog is required for watch mode. Install it with: pip install watchdog",
|
|
404
663
|
file=sys.stderr,
|
|
405
664
|
)
|
|
@@ -414,7 +673,7 @@ def gen_tsi(
|
|
|
414
673
|
else str(event.src_path)
|
|
415
674
|
)
|
|
416
675
|
if not event.is_directory and src_path.endswith(".py"):
|
|
417
|
-
|
|
676
|
+
click.echo(f"File changed: {src_path}")
|
|
418
677
|
# Create a completely detached process to ensure classes are reloaded
|
|
419
678
|
process = multiprocessing.get_context("spawn").Process(
|
|
420
679
|
target=generate_interfaces,
|
|
@@ -445,16 +704,16 @@ def gen_tsi(
|
|
|
445
704
|
|
|
446
705
|
# Set up observer
|
|
447
706
|
observer = Observer()
|
|
448
|
-
observer.schedule(PyFileChangeHandler(), src_dir, recursive=True)
|
|
449
|
-
observer.start()
|
|
707
|
+
observer.schedule(PyFileChangeHandler(), src_dir, recursive=True) # type: ignore
|
|
708
|
+
observer.start() # type: ignore
|
|
450
709
|
|
|
451
|
-
|
|
710
|
+
click.echo(f"Watching for changes in {os.path.abspath(src_dir)}...")
|
|
452
711
|
try:
|
|
453
712
|
while True:
|
|
454
713
|
time.sleep(1)
|
|
455
714
|
except KeyboardInterrupt:
|
|
456
|
-
observer.stop()
|
|
457
|
-
|
|
715
|
+
observer.stop() # type: ignore
|
|
716
|
+
click.echo("Watch mode stopped")
|
|
458
717
|
observer.join()
|
|
459
718
|
|
|
460
719
|
|
|
@@ -491,3 +750,88 @@ def gen_entity(entity_name: str, file_path: StreamWriter) -> None:
|
|
|
491
750
|
entityNameKebabCase=entity_kebab_case,
|
|
492
751
|
)
|
|
493
752
|
)
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
@cli.command()
|
|
756
|
+
@click.argument(
|
|
757
|
+
"app_path",
|
|
758
|
+
type=str,
|
|
759
|
+
envvar="APP_PATH",
|
|
760
|
+
)
|
|
761
|
+
@click.option(
|
|
762
|
+
"--broker-url",
|
|
763
|
+
type=str,
|
|
764
|
+
envvar="BROKER_URL",
|
|
765
|
+
help="Broker URL (e.g., amqp://guest:guest@localhost/) [env: BROKER_URL]",
|
|
766
|
+
)
|
|
767
|
+
@click.option(
|
|
768
|
+
"-i",
|
|
769
|
+
"--interactive-mode",
|
|
770
|
+
is_flag=True,
|
|
771
|
+
default=False,
|
|
772
|
+
help="Enable interactive mode for queue declaration (confirm before deleting existing queues)",
|
|
773
|
+
)
|
|
774
|
+
@click.option(
|
|
775
|
+
"-f",
|
|
776
|
+
"--force",
|
|
777
|
+
is_flag=True,
|
|
778
|
+
default=False,
|
|
779
|
+
help="Force recreation by deleting existing exchanges and queues before declaring them",
|
|
780
|
+
)
|
|
781
|
+
def declare(
|
|
782
|
+
app_path: str,
|
|
783
|
+
broker_url: str,
|
|
784
|
+
force: bool,
|
|
785
|
+
interactive_mode: bool,
|
|
786
|
+
) -> None:
|
|
787
|
+
"""
|
|
788
|
+
Declare RabbitMQ infrastructure (exchanges and queues) for message handlers and schedulers.
|
|
789
|
+
|
|
790
|
+
This command pre-declares the necessary exchanges and queues for message handlers and schedulers,
|
|
791
|
+
without starting the actual consumption processes.
|
|
792
|
+
|
|
793
|
+
Environment variables:
|
|
794
|
+
- BROKER_URL: Broker URL (e.g., amqp://guest:guest@localhost/)
|
|
795
|
+
- EXCHANGE: Exchange name (defaults to 'jararaca_ex')
|
|
796
|
+
|
|
797
|
+
Examples:
|
|
798
|
+
|
|
799
|
+
\b
|
|
800
|
+
# Declare infrastructure
|
|
801
|
+
jararaca declare myapp:app --broker-url amqp://guest:guest@localhost/
|
|
802
|
+
|
|
803
|
+
\b
|
|
804
|
+
# Force recreation of queues and exchanges
|
|
805
|
+
jararaca declare myapp:app --broker-url amqp://guest:guest@localhost/ --force
|
|
806
|
+
|
|
807
|
+
\b
|
|
808
|
+
# Use environment variables
|
|
809
|
+
export BROKER_URL="amqp://guest:guest@localhost/"
|
|
810
|
+
export EXCHANGE="my_exchange"
|
|
811
|
+
jararaca declare myapp:app
|
|
812
|
+
"""
|
|
813
|
+
|
|
814
|
+
app = find_microservice_by_module_path(app_path)
|
|
815
|
+
|
|
816
|
+
async def run_declarations() -> None:
|
|
817
|
+
if not broker_url:
|
|
818
|
+
click.echo(
|
|
819
|
+
"ERROR: --broker-url is required or set BROKER_URL environment variable",
|
|
820
|
+
err=True,
|
|
821
|
+
)
|
|
822
|
+
return
|
|
823
|
+
|
|
824
|
+
try:
|
|
825
|
+
# Create the broker URL with exchange parameter
|
|
826
|
+
|
|
827
|
+
click.echo(f"→ Declaring worker infrastructure (URL: {broker_url})")
|
|
828
|
+
await declare_worker_infrastructure(
|
|
829
|
+
broker_url, app, force, interactive_mode
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
click.echo("✓ Workers infrastructure declared successfully!")
|
|
833
|
+
except Exception as e:
|
|
834
|
+
click.echo(f"ERROR: Failed to declare infrastructure: {e}", err=True)
|
|
835
|
+
raise
|
|
836
|
+
|
|
837
|
+
asyncio.run(run_declarations())
|