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.
- README.md +121 -0
- jararaca/__init__.py +184 -12
- jararaca/__main__.py +4 -0
- jararaca/broker_backend/__init__.py +4 -0
- jararaca/broker_backend/mapper.py +4 -0
- jararaca/broker_backend/redis_broker_backend.py +9 -3
- jararaca/cli.py +272 -47
- jararaca/common/__init__.py +3 -0
- jararaca/core/__init__.py +3 -0
- jararaca/core/providers.py +4 -0
- jararaca/core/uow.py +41 -7
- jararaca/di.py +4 -0
- jararaca/files/entity.py.mako +4 -0
- jararaca/lifecycle.py +6 -2
- jararaca/messagebus/__init__.py +4 -0
- jararaca/messagebus/bus_message_controller.py +4 -0
- jararaca/messagebus/consumers/__init__.py +3 -0
- jararaca/messagebus/decorators.py +33 -67
- jararaca/messagebus/implicit_headers.py +49 -0
- jararaca/messagebus/interceptors/__init__.py +3 -0
- jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +13 -4
- jararaca/messagebus/interceptors/publisher_interceptor.py +4 -0
- jararaca/messagebus/message.py +4 -0
- jararaca/messagebus/publisher.py +6 -0
- jararaca/messagebus/worker.py +850 -383
- jararaca/microservice.py +110 -1
- jararaca/observability/constants.py +7 -0
- jararaca/observability/decorators.py +170 -13
- jararaca/observability/fastapi_exception_handler.py +37 -0
- jararaca/observability/hooks.py +109 -0
- jararaca/observability/interceptor.py +4 -0
- jararaca/observability/providers/__init__.py +3 -0
- jararaca/observability/providers/otel.py +202 -11
- jararaca/persistence/base.py +38 -2
- jararaca/persistence/exports.py +4 -0
- jararaca/persistence/interceptors/__init__.py +3 -0
- jararaca/persistence/interceptors/aiosqa_interceptor.py +86 -73
- jararaca/persistence/interceptors/constants.py +5 -0
- jararaca/persistence/interceptors/decorators.py +50 -0
- jararaca/persistence/session.py +3 -0
- jararaca/persistence/sort_filter.py +4 -0
- jararaca/persistence/utilities.py +50 -20
- jararaca/presentation/__init__.py +3 -0
- jararaca/presentation/decorators.py +88 -86
- jararaca/presentation/exceptions.py +23 -0
- jararaca/presentation/hooks.py +4 -0
- jararaca/presentation/http_microservice.py +4 -0
- jararaca/presentation/server.py +97 -45
- jararaca/presentation/websocket/__init__.py +3 -0
- jararaca/presentation/websocket/base_types.py +4 -0
- jararaca/presentation/websocket/context.py +4 -0
- jararaca/presentation/websocket/decorators.py +8 -41
- jararaca/presentation/websocket/redis.py +280 -53
- jararaca/presentation/websocket/types.py +4 -0
- jararaca/presentation/websocket/websocket_interceptor.py +46 -19
- jararaca/reflect/__init__.py +3 -0
- jararaca/reflect/controller_inspect.py +16 -10
- jararaca/reflect/decorators.py +238 -0
- jararaca/reflect/metadata.py +34 -25
- jararaca/rpc/__init__.py +3 -0
- jararaca/rpc/http/__init__.py +101 -0
- jararaca/rpc/http/backends/__init__.py +14 -0
- jararaca/rpc/http/backends/httpx.py +43 -9
- jararaca/rpc/http/backends/otel.py +4 -0
- jararaca/rpc/http/decorators.py +378 -113
- jararaca/rpc/http/httpx.py +3 -0
- jararaca/scheduler/__init__.py +3 -0
- jararaca/scheduler/beat_worker.py +521 -105
- jararaca/scheduler/decorators.py +15 -22
- jararaca/scheduler/types.py +4 -0
- jararaca/tools/app_config/__init__.py +3 -0
- jararaca/tools/app_config/decorators.py +7 -19
- jararaca/tools/app_config/interceptor.py +6 -2
- jararaca/tools/typescript/__init__.py +3 -0
- jararaca/tools/typescript/decorators.py +120 -0
- jararaca/tools/typescript/interface_parser.py +1074 -173
- jararaca/utils/__init__.py +3 -0
- jararaca/utils/rabbitmq_utils.py +65 -39
- jararaca/utils/retry.py +10 -3
- jararaca-0.4.0a5.dist-info/LICENSE +674 -0
- jararaca-0.4.0a5.dist-info/LICENSES/GPL-3.0-or-later.txt +232 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/METADATA +11 -7
- jararaca-0.4.0a5.dist-info/RECORD +88 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/WHEEL +1 -1
- pyproject.toml +131 -0
- jararaca-0.3.11a16.dist-info/RECORD +0 -74
- /jararaca-0.3.11a16.dist-info/LICENSE → /LICENSE +0 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/entry_points.txt +0 -0
jararaca/cli.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
1
5
|
import asyncio
|
|
2
6
|
import importlib
|
|
3
7
|
import importlib.resources
|
|
@@ -5,9 +9,11 @@ import multiprocessing
|
|
|
5
9
|
import os
|
|
6
10
|
import sys
|
|
7
11
|
import time
|
|
12
|
+
import traceback
|
|
13
|
+
import typing
|
|
8
14
|
from codecs import StreamWriter
|
|
9
15
|
from pathlib import Path
|
|
10
|
-
from typing import Any
|
|
16
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
11
17
|
from urllib.parse import parse_qs, urlparse
|
|
12
18
|
|
|
13
19
|
import aio_pika
|
|
@@ -31,6 +37,9 @@ from jararaca.tools.typescript.interface_parser import (
|
|
|
31
37
|
)
|
|
32
38
|
from jararaca.utils.rabbitmq_utils import RabbitmqUtils
|
|
33
39
|
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from watchdog.observers.api import BaseObserver
|
|
42
|
+
|
|
34
43
|
LIBRARY_FILES_PATH = importlib.resources.files("jararaca.files")
|
|
35
44
|
ENTITY_TEMPLATE_PATH = LIBRARY_FILES_PATH / "entity.py.mako"
|
|
36
45
|
|
|
@@ -256,7 +265,7 @@ async def declare_controller_queues(
|
|
|
256
265
|
Declare all message handler and scheduled action queues for controllers.
|
|
257
266
|
"""
|
|
258
267
|
for instance_type in app.controllers:
|
|
259
|
-
controller_spec = MessageBusController.
|
|
268
|
+
controller_spec = MessageBusController.get_last(instance_type)
|
|
260
269
|
if controller_spec is None:
|
|
261
270
|
continue
|
|
262
271
|
|
|
@@ -266,7 +275,7 @@ async def declare_controller_queues(
|
|
|
266
275
|
for _, member in members.items():
|
|
267
276
|
# Check if it's a message handler
|
|
268
277
|
await declare_message_handler_queue(
|
|
269
|
-
connection, member, exchange, force, interactive_mode
|
|
278
|
+
connection, member, exchange, force, interactive_mode, controller_spec
|
|
270
279
|
)
|
|
271
280
|
|
|
272
281
|
# Check if it's a scheduled action
|
|
@@ -281,11 +290,12 @@ async def declare_message_handler_queue(
|
|
|
281
290
|
exchange: str,
|
|
282
291
|
force: bool,
|
|
283
292
|
interactive_mode: bool,
|
|
293
|
+
controller_spec: MessageBusController,
|
|
284
294
|
) -> None:
|
|
285
295
|
"""
|
|
286
296
|
Declare a queue for a message handler if the member is one.
|
|
287
297
|
"""
|
|
288
|
-
message_handler = MessageHandler.
|
|
298
|
+
message_handler = MessageHandler.get_last(member.member_function)
|
|
289
299
|
if message_handler is not None:
|
|
290
300
|
queue_name = f"{message_handler.message_type.MESSAGE_TOPIC}.{member.member_function.__module__}.{member.member_function.__qualname__}"
|
|
291
301
|
routing_key = f"{message_handler.message_type.MESSAGE_TOPIC}.#"
|
|
@@ -311,7 +321,7 @@ async def declare_scheduled_action_queue(
|
|
|
311
321
|
"""
|
|
312
322
|
Declare a queue for a scheduled action if the member is one.
|
|
313
323
|
"""
|
|
314
|
-
scheduled_action = ScheduledAction.
|
|
324
|
+
scheduled_action = ScheduledAction.get_last(member.member_function)
|
|
315
325
|
if scheduled_action is not None:
|
|
316
326
|
queue_name = (
|
|
317
327
|
f"{member.member_function.__module__}.{member.member_function.__qualname__}"
|
|
@@ -421,25 +431,52 @@ def cli() -> None:
|
|
|
421
431
|
@click.option(
|
|
422
432
|
"--handlers",
|
|
423
433
|
type=str,
|
|
434
|
+
envvar="HANDLERS",
|
|
424
435
|
help="Comma-separated list of handler names to listen to. If not specified, all handlers will be used.",
|
|
425
436
|
)
|
|
437
|
+
@click.option(
|
|
438
|
+
"--reload",
|
|
439
|
+
is_flag=True,
|
|
440
|
+
envvar="RELOAD",
|
|
441
|
+
help="Enable auto-reload when Python files change.",
|
|
442
|
+
)
|
|
443
|
+
@click.option(
|
|
444
|
+
"--src-dir",
|
|
445
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
446
|
+
default="src",
|
|
447
|
+
envvar="SRC_DIR",
|
|
448
|
+
help="The source directory to watch for changes when --reload is enabled.",
|
|
449
|
+
)
|
|
450
|
+
@click.option(
|
|
451
|
+
"--gracious-shutdown-seconds",
|
|
452
|
+
type=int,
|
|
453
|
+
default=20,
|
|
454
|
+
envvar="GRACIOUS_SHUTDOWN_SECONDS",
|
|
455
|
+
help="Number of seconds to wait for graceful shutdown on reload",
|
|
456
|
+
)
|
|
426
457
|
def worker(
|
|
427
|
-
app_path: str,
|
|
458
|
+
app_path: str,
|
|
459
|
+
broker_url: str,
|
|
460
|
+
backend_url: str,
|
|
461
|
+
handlers: str | None,
|
|
462
|
+
reload: bool,
|
|
463
|
+
src_dir: str,
|
|
464
|
+
gracious_shutdown_seconds: int,
|
|
428
465
|
) -> None:
|
|
429
466
|
"""Start a message bus worker that processes asynchronous messages from a message queue."""
|
|
430
|
-
app = find_microservice_by_module_path(app_path)
|
|
431
467
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
468
|
+
if reload:
|
|
469
|
+
process_args = {
|
|
470
|
+
"app_path": app_path,
|
|
471
|
+
"broker_url": broker_url,
|
|
472
|
+
"backend_url": backend_url,
|
|
473
|
+
"handlers": handlers,
|
|
474
|
+
}
|
|
475
|
+
run_with_reload_watcher(
|
|
476
|
+
process_args, run_worker_process, src_dir, gracious_shutdown_seconds
|
|
477
|
+
)
|
|
478
|
+
else:
|
|
479
|
+
run_worker_process(app_path, broker_url, backend_url, handlers)
|
|
443
480
|
|
|
444
481
|
|
|
445
482
|
@cli.command()
|
|
@@ -485,51 +522,78 @@ def server(app_path: str, host: str, port: int) -> None:
|
|
|
485
522
|
@click.argument(
|
|
486
523
|
"app_path",
|
|
487
524
|
type=str,
|
|
525
|
+
envvar="APP_PATH",
|
|
488
526
|
)
|
|
489
527
|
@click.option(
|
|
490
528
|
"--interval",
|
|
491
529
|
type=int,
|
|
492
530
|
default=1,
|
|
493
531
|
required=True,
|
|
532
|
+
envvar="INTERVAL",
|
|
494
533
|
)
|
|
495
534
|
@click.option(
|
|
496
535
|
"--broker-url",
|
|
497
536
|
type=str,
|
|
498
537
|
required=True,
|
|
538
|
+
envvar="BROKER_URL",
|
|
499
539
|
)
|
|
500
540
|
@click.option(
|
|
501
541
|
"--backend-url",
|
|
502
542
|
type=str,
|
|
503
543
|
required=True,
|
|
544
|
+
envvar="BACKEND_URL",
|
|
504
545
|
)
|
|
505
546
|
@click.option(
|
|
506
547
|
"--actions",
|
|
507
548
|
type=str,
|
|
549
|
+
envvar="ACTIONS",
|
|
508
550
|
help="Comma-separated list of action names to run (only run actions with these names)",
|
|
509
551
|
)
|
|
552
|
+
@click.option(
|
|
553
|
+
"--reload",
|
|
554
|
+
is_flag=True,
|
|
555
|
+
envvar="RELOAD",
|
|
556
|
+
help="Enable auto-reload when Python files change.",
|
|
557
|
+
)
|
|
558
|
+
@click.option(
|
|
559
|
+
"--src-dir",
|
|
560
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
561
|
+
default="src",
|
|
562
|
+
envvar="SRC_DIR",
|
|
563
|
+
help="The source directory to watch for changes when --reload is enabled.",
|
|
564
|
+
)
|
|
565
|
+
@click.option(
|
|
566
|
+
"--gracious-shutdown-seconds",
|
|
567
|
+
type=int,
|
|
568
|
+
default=20,
|
|
569
|
+
envvar="GRACIOUS_SHUTDOWN_SECONDS",
|
|
570
|
+
help="Number of seconds to wait for graceful shutdown on reload",
|
|
571
|
+
)
|
|
510
572
|
def beat(
|
|
511
573
|
interval: int,
|
|
512
574
|
broker_url: str,
|
|
513
575
|
backend_url: str,
|
|
514
576
|
app_path: str,
|
|
515
577
|
actions: str | None = None,
|
|
578
|
+
reload: bool = False,
|
|
579
|
+
src_dir: str = "src",
|
|
580
|
+
gracious_shutdown_seconds: int = 20,
|
|
516
581
|
) -> None:
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
beat_worker.run()
|
|
582
|
+
"""Start a scheduler that dispatches scheduled actions to workers."""
|
|
583
|
+
|
|
584
|
+
if reload:
|
|
585
|
+
process_args = {
|
|
586
|
+
"app_path": app_path,
|
|
587
|
+
"interval": interval,
|
|
588
|
+
"broker_url": broker_url,
|
|
589
|
+
"backend_url": backend_url,
|
|
590
|
+
"actions": actions,
|
|
591
|
+
}
|
|
592
|
+
run_with_reload_watcher(
|
|
593
|
+
process_args, run_beat_process, src_dir, gracious_shutdown_seconds
|
|
594
|
+
)
|
|
595
|
+
else:
|
|
596
|
+
run_beat_process(app_path, interval, broker_url, backend_url, actions)
|
|
533
597
|
|
|
534
598
|
|
|
535
599
|
def generate_interfaces(
|
|
@@ -581,6 +645,7 @@ def generate_interfaces(
|
|
|
581
645
|
return content
|
|
582
646
|
except Exception as e:
|
|
583
647
|
click.echo(f"Error generating TypeScript interfaces: {e}", file=sys.stderr)
|
|
648
|
+
traceback.print_exc(file=sys.stderr)
|
|
584
649
|
return ""
|
|
585
650
|
|
|
586
651
|
|
|
@@ -588,29 +653,35 @@ def generate_interfaces(
|
|
|
588
653
|
@click.argument(
|
|
589
654
|
"app_path",
|
|
590
655
|
type=str,
|
|
656
|
+
envvar="APP_PATH",
|
|
591
657
|
)
|
|
592
658
|
@click.argument(
|
|
593
659
|
"file_path",
|
|
594
660
|
type=click.Path(file_okay=True, dir_okay=False),
|
|
595
661
|
required=False,
|
|
662
|
+
envvar="FILE_PATH",
|
|
596
663
|
)
|
|
597
664
|
@click.option(
|
|
598
665
|
"--watch",
|
|
599
666
|
is_flag=True,
|
|
667
|
+
envvar="WATCH",
|
|
600
668
|
)
|
|
601
669
|
@click.option(
|
|
602
670
|
"--src-dir",
|
|
603
671
|
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
604
672
|
default="src",
|
|
673
|
+
envvar="SRC_DIR",
|
|
605
674
|
)
|
|
606
675
|
@click.option(
|
|
607
676
|
"--stdout",
|
|
608
677
|
is_flag=True,
|
|
678
|
+
envvar="STDOUT",
|
|
609
679
|
help="Print generated interfaces to stdout instead of writing to a file",
|
|
610
680
|
)
|
|
611
681
|
@click.option(
|
|
612
682
|
"--post-process",
|
|
613
683
|
type=str,
|
|
684
|
+
envvar="POST_PROCESS",
|
|
614
685
|
help="Command to run after generating the interfaces, {file} will be replaced with the output file path",
|
|
615
686
|
)
|
|
616
687
|
def gen_tsi(
|
|
@@ -702,19 +773,23 @@ def gen_tsi(
|
|
|
702
773
|
|
|
703
774
|
# subprocess.run(cmd, check=False)
|
|
704
775
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
observer.schedule(PyFileChangeHandler(), src_dir, recursive=True) # type: ignore
|
|
708
|
-
observer.start() # type: ignore
|
|
776
|
+
@typing.no_type_check
|
|
777
|
+
def start_watchdog() -> None:
|
|
709
778
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
779
|
+
observer: "BaseObserver" = Observer()
|
|
780
|
+
observer.schedule(PyFileChangeHandler(), src_dir, recursive=True)
|
|
781
|
+
observer.start()
|
|
782
|
+
|
|
783
|
+
click.echo(f"Watching for changes in {os.path.abspath(src_dir)}...")
|
|
784
|
+
try:
|
|
785
|
+
while True:
|
|
786
|
+
time.sleep(1)
|
|
787
|
+
except KeyboardInterrupt:
|
|
788
|
+
observer.stop()
|
|
789
|
+
click.echo("Watch mode stopped")
|
|
790
|
+
observer.join()
|
|
791
|
+
|
|
792
|
+
start_watchdog()
|
|
718
793
|
|
|
719
794
|
|
|
720
795
|
def camel_case_to_snake_case(name: str) -> str:
|
|
@@ -730,10 +805,11 @@ def camel_case_to_pascal_case(name: str) -> str:
|
|
|
730
805
|
|
|
731
806
|
|
|
732
807
|
@cli.command()
|
|
733
|
-
@click.argument("entity_name", type=click.STRING)
|
|
808
|
+
@click.argument("entity_name", type=click.STRING, envvar="ENTITY_NAME")
|
|
734
809
|
@click.argument(
|
|
735
810
|
"file_path",
|
|
736
811
|
type=click.File("w"),
|
|
812
|
+
envvar="FILE_PATH",
|
|
737
813
|
)
|
|
738
814
|
def gen_entity(entity_name: str, file_path: StreamWriter) -> None:
|
|
739
815
|
|
|
@@ -769,6 +845,7 @@ def gen_entity(entity_name: str, file_path: StreamWriter) -> None:
|
|
|
769
845
|
"--interactive-mode",
|
|
770
846
|
is_flag=True,
|
|
771
847
|
default=False,
|
|
848
|
+
envvar="INTERACTIVE_MODE",
|
|
772
849
|
help="Enable interactive mode for queue declaration (confirm before deleting existing queues)",
|
|
773
850
|
)
|
|
774
851
|
@click.option(
|
|
@@ -776,6 +853,7 @@ def gen_entity(entity_name: str, file_path: StreamWriter) -> None:
|
|
|
776
853
|
"--force",
|
|
777
854
|
is_flag=True,
|
|
778
855
|
default=False,
|
|
856
|
+
envvar="FORCE",
|
|
779
857
|
help="Force recreation by deleting existing exchanges and queues before declaring them",
|
|
780
858
|
)
|
|
781
859
|
def declare(
|
|
@@ -835,3 +913,150 @@ def declare(
|
|
|
835
913
|
raise
|
|
836
914
|
|
|
837
915
|
asyncio.run(run_declarations())
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def run_worker_process(
|
|
919
|
+
app_path: str, broker_url: str, backend_url: str, handlers: str | None
|
|
920
|
+
) -> None:
|
|
921
|
+
"""Run a worker process with the given parameters."""
|
|
922
|
+
app = find_microservice_by_module_path(app_path)
|
|
923
|
+
|
|
924
|
+
# Parse handler names if provided
|
|
925
|
+
handler_names: set[str] | None = None
|
|
926
|
+
if handlers:
|
|
927
|
+
handler_names = {name.strip() for name in handlers.split(",") if name.strip()}
|
|
928
|
+
|
|
929
|
+
click.echo(f"Starting worker for {app_path}...")
|
|
930
|
+
worker_mod.MessageBusWorker(
|
|
931
|
+
app=app,
|
|
932
|
+
broker_url=broker_url,
|
|
933
|
+
backend_url=backend_url,
|
|
934
|
+
handler_names=handler_names,
|
|
935
|
+
).start_sync()
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
def run_beat_process(
|
|
939
|
+
app_path: str, interval: int, broker_url: str, backend_url: str, actions: str | None
|
|
940
|
+
) -> None:
|
|
941
|
+
"""Run a beat scheduler process with the given parameters."""
|
|
942
|
+
app = find_microservice_by_module_path(app_path)
|
|
943
|
+
|
|
944
|
+
# Parse scheduler names if provided
|
|
945
|
+
scheduler_names: set[str] | None = None
|
|
946
|
+
if actions:
|
|
947
|
+
scheduler_names = {name.strip() for name in actions.split(",") if name.strip()}
|
|
948
|
+
|
|
949
|
+
click.echo(f"Starting beat scheduler for {app_path}...")
|
|
950
|
+
beat_worker = BeatWorker(
|
|
951
|
+
app=app,
|
|
952
|
+
interval=interval,
|
|
953
|
+
backend_url=backend_url,
|
|
954
|
+
broker_url=broker_url,
|
|
955
|
+
scheduled_action_names=scheduler_names,
|
|
956
|
+
)
|
|
957
|
+
beat_worker.run()
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
def run_with_reload_watcher(
|
|
961
|
+
process_args: dict[str, Any],
|
|
962
|
+
process_target: Callable[..., Any],
|
|
963
|
+
src_dir: str = "src",
|
|
964
|
+
max_graceful_shutdown_seconds: int = 20,
|
|
965
|
+
) -> None:
|
|
966
|
+
"""
|
|
967
|
+
Run a process with a file watcher that will restart it when Python files change.
|
|
968
|
+
|
|
969
|
+
Args:
|
|
970
|
+
process_args: Arguments to pass to the process function
|
|
971
|
+
process_target: The function to run as the process
|
|
972
|
+
src_dir: The directory to watch for changes
|
|
973
|
+
"""
|
|
974
|
+
try:
|
|
975
|
+
from watchdog.events import FileSystemEvent, FileSystemEventHandler
|
|
976
|
+
from watchdog.observers import Observer
|
|
977
|
+
except ImportError:
|
|
978
|
+
click.echo(
|
|
979
|
+
"Watchdog is required for reload mode. Install it with: pip install watchdog",
|
|
980
|
+
file=sys.stderr,
|
|
981
|
+
)
|
|
982
|
+
return
|
|
983
|
+
|
|
984
|
+
# Run the initial process
|
|
985
|
+
process = multiprocessing.get_context("spawn").Process(
|
|
986
|
+
target=process_target,
|
|
987
|
+
kwargs=process_args,
|
|
988
|
+
daemon=False, # Non-daemon to ensure it completes properly
|
|
989
|
+
)
|
|
990
|
+
process.start() # Set up file system event handler
|
|
991
|
+
|
|
992
|
+
class PyFileChangeHandler(FileSystemEventHandler):
|
|
993
|
+
def __init__(self) -> None:
|
|
994
|
+
self.last_modified_time = time.time()
|
|
995
|
+
self.debounce_seconds = 1.0 # Debounce to avoid multiple restarts
|
|
996
|
+
self.active_process = process
|
|
997
|
+
|
|
998
|
+
def on_modified(self, event: FileSystemEvent) -> None:
|
|
999
|
+
src_path = (
|
|
1000
|
+
event.src_path
|
|
1001
|
+
if isinstance(event.src_path, str)
|
|
1002
|
+
else str(event.src_path)
|
|
1003
|
+
)
|
|
1004
|
+
|
|
1005
|
+
# Ignore non-Python files and directories
|
|
1006
|
+
if event.is_directory or not src_path.endswith(".py"):
|
|
1007
|
+
return
|
|
1008
|
+
|
|
1009
|
+
# Debounce to avoid multiple restarts
|
|
1010
|
+
current_time = time.time()
|
|
1011
|
+
if current_time - self.last_modified_time < self.debounce_seconds:
|
|
1012
|
+
return
|
|
1013
|
+
self.last_modified_time = current_time
|
|
1014
|
+
|
|
1015
|
+
click.echo(f"Detected change in {src_path}")
|
|
1016
|
+
click.echo("Restarting process...")
|
|
1017
|
+
|
|
1018
|
+
# Terminate the current process
|
|
1019
|
+
if self.active_process and self.active_process.is_alive():
|
|
1020
|
+
self.active_process.terminate()
|
|
1021
|
+
self.active_process.join(timeout=max_graceful_shutdown_seconds)
|
|
1022
|
+
|
|
1023
|
+
# If process doesn't terminate, kill it
|
|
1024
|
+
if self.active_process.is_alive():
|
|
1025
|
+
click.echo("Process did not terminate gracefully, killing it")
|
|
1026
|
+
self.active_process.kill()
|
|
1027
|
+
self.active_process.join()
|
|
1028
|
+
|
|
1029
|
+
# Create a new process
|
|
1030
|
+
|
|
1031
|
+
self.active_process = multiprocessing.get_context("spawn").Process(
|
|
1032
|
+
target=process_target,
|
|
1033
|
+
kwargs=process_args,
|
|
1034
|
+
daemon=False,
|
|
1035
|
+
)
|
|
1036
|
+
self.active_process.start()
|
|
1037
|
+
|
|
1038
|
+
@typing.no_type_check
|
|
1039
|
+
def start_watchdog() -> None:
|
|
1040
|
+
|
|
1041
|
+
# Set up observer
|
|
1042
|
+
observer = Observer()
|
|
1043
|
+
observer.schedule(PyFileChangeHandler(), src_dir, recursive=True)
|
|
1044
|
+
observer.start()
|
|
1045
|
+
|
|
1046
|
+
click.echo(f"Watching for changes in {os.path.abspath(src_dir)}...")
|
|
1047
|
+
try:
|
|
1048
|
+
while True:
|
|
1049
|
+
time.sleep(1)
|
|
1050
|
+
except KeyboardInterrupt:
|
|
1051
|
+
observer.stop()
|
|
1052
|
+
if process.is_alive():
|
|
1053
|
+
click.echo("Stopping process...")
|
|
1054
|
+
process.terminate()
|
|
1055
|
+
process.join(timeout=max_graceful_shutdown_seconds)
|
|
1056
|
+
if process.is_alive():
|
|
1057
|
+
process.kill()
|
|
1058
|
+
process.join()
|
|
1059
|
+
click.echo("Reload mode stopped")
|
|
1060
|
+
observer.join()
|
|
1061
|
+
|
|
1062
|
+
start_watchdog()
|
jararaca/common/__init__.py
CHANGED
jararaca/core/__init__.py
CHANGED
jararaca/core/providers.py
CHANGED
jararaca/core/uow.py
CHANGED
|
@@ -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 AsyncGenerator, Sequence
|
|
3
7
|
|
|
@@ -9,7 +13,7 @@ from jararaca.microservice import (
|
|
|
9
13
|
provide_app_context,
|
|
10
14
|
provide_container,
|
|
11
15
|
)
|
|
12
|
-
from jararaca.reflect.metadata import
|
|
16
|
+
from jararaca.reflect.metadata import start_transaction_metadata_context
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
class ContainerInterceptor(AppInterceptor):
|
|
@@ -33,7 +37,6 @@ class UnitOfWorkContextProvider:
|
|
|
33
37
|
self.container = container
|
|
34
38
|
self.container_interceptor = ContainerInterceptor(container)
|
|
35
39
|
|
|
36
|
-
# TODO: Guarantee that the context is closed whenever an exception is raised
|
|
37
40
|
# TODO: Guarantee a unit of work workflow for the whole request, including all the interceptors
|
|
38
41
|
|
|
39
42
|
def factory_app_interceptors(self) -> Sequence[AppInterceptor]:
|
|
@@ -57,7 +60,9 @@ class UnitOfWorkContextProvider:
|
|
|
57
60
|
) -> AsyncGenerator[None, None]:
|
|
58
61
|
|
|
59
62
|
app_interceptors = self.factory_app_interceptors()
|
|
60
|
-
with
|
|
63
|
+
with start_transaction_metadata_context(
|
|
64
|
+
app_context.controller_member_reflect.metadata
|
|
65
|
+
):
|
|
61
66
|
ctxs = [self.container_interceptor.intercept(app_context)] + [
|
|
62
67
|
interceptor.intercept(app_context) for interceptor in app_interceptors
|
|
63
68
|
]
|
|
@@ -65,7 +70,36 @@ class UnitOfWorkContextProvider:
|
|
|
65
70
|
for ctx in ctxs:
|
|
66
71
|
await ctx.__aenter__()
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
exc_type = None
|
|
74
|
+
exc_value = None
|
|
75
|
+
exc_traceback = None
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
yield None
|
|
79
|
+
except BaseException as e:
|
|
80
|
+
exc_type = type(e)
|
|
81
|
+
exc_value = e
|
|
82
|
+
exc_traceback = e.__traceback__
|
|
83
|
+
raise
|
|
84
|
+
finally:
|
|
85
|
+
# Exit interceptors in reverse order, propagating exception info
|
|
86
|
+
for ctx in reversed(ctxs):
|
|
87
|
+
try:
|
|
88
|
+
suppressed = await ctx.__aexit__(
|
|
89
|
+
exc_type, exc_value, exc_traceback
|
|
90
|
+
)
|
|
91
|
+
# If an interceptor returns True, it suppresses the exception
|
|
92
|
+
if suppressed and exc_type is not None:
|
|
93
|
+
exc_type = None
|
|
94
|
+
exc_value = None
|
|
95
|
+
exc_traceback = None
|
|
96
|
+
except BaseException as exit_exc:
|
|
97
|
+
# If an interceptor raises an exception during cleanup,
|
|
98
|
+
# replace the original exception with the new one
|
|
99
|
+
exc_type = type(exit_exc)
|
|
100
|
+
exc_value = exit_exc
|
|
101
|
+
exc_traceback = exit_exc.__traceback__
|
|
102
|
+
|
|
103
|
+
# Re-raise the exception if it wasn't suppressed
|
|
104
|
+
if exc_type is not None and exc_value is not None:
|
|
105
|
+
raise exc_value.with_traceback(exc_traceback)
|
jararaca/di.py
CHANGED
jararaca/files/entity.py.mako
CHANGED
jararaca/lifecycle.py
CHANGED
|
@@ -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 typing import AsyncContextManager, AsyncGenerator, Sequence
|
|
@@ -33,7 +37,7 @@ class AppLifecycle:
|
|
|
33
37
|
self.container.fill_providers(False)
|
|
34
38
|
lifecycle_ctxs: list[AsyncContextManager[None]] = []
|
|
35
39
|
|
|
36
|
-
logger.
|
|
40
|
+
logger.debug("Initializing interceptors lifecycle")
|
|
37
41
|
for interceptor_dep in self.app.interceptors:
|
|
38
42
|
interceptor: AppInterceptor
|
|
39
43
|
if not isinstance(interceptor_dep, AppInterceptor):
|
|
@@ -57,6 +61,6 @@ class AppLifecycle:
|
|
|
57
61
|
|
|
58
62
|
yield
|
|
59
63
|
|
|
60
|
-
logger.
|
|
64
|
+
logger.debug("Finalizing interceptors lifecycle")
|
|
61
65
|
for ctx in lifecycle_ctxs:
|
|
62
66
|
await ctx.__aexit__(None, None, None)
|
jararaca/messagebus/__init__.py
CHANGED