jararaca 0.3.11a15__py3-none-any.whl → 0.3.12a0__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.
- README.md +120 -0
- jararaca/cli.py +214 -31
- jararaca/messagebus/worker.py +861 -89
- jararaca/utils/retry.py +141 -0
- jararaca-0.3.12a0.dist-info/LICENSE +674 -0
- {jararaca-0.3.11a15.dist-info → jararaca-0.3.12a0.dist-info}/METADATA +3 -3
- {jararaca-0.3.11a15.dist-info → jararaca-0.3.12a0.dist-info}/RECORD +11 -7
- {jararaca-0.3.11a15.dist-info → jararaca-0.3.12a0.dist-info}/WHEEL +1 -1
- pyproject.toml +85 -0
- /jararaca-0.3.11a15.dist-info/LICENSE → /LICENSE +0 -0
- {jararaca-0.3.11a15.dist-info → jararaca-0.3.12a0.dist-info}/entry_points.txt +0 -0
README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<img src="https://raw.githubusercontent.com/LuscasLeo/jararaca/main/docs/assets/_f04774c9-7e05-4da4-8b17-8be23f6a1475.jpeg" alt="Jararaca Logo" width="250" float="right">
|
|
2
|
+
|
|
3
|
+
# Jararaca Microservice Framework
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Jararaca is an async-first microservice framework designed to simplify the development of distributed systems. It provides a comprehensive set of tools for building robust, scalable, and maintainable microservices with a focus on developer experience and type safety.
|
|
8
|
+
|
|
9
|
+
## Key Features
|
|
10
|
+
|
|
11
|
+
### REST API Development
|
|
12
|
+
- Easy-to-use interfaces for building REST APIs
|
|
13
|
+
- Automatic request/response validation
|
|
14
|
+
- Type-safe endpoints with FastAPI integration
|
|
15
|
+
- Automatic OpenAPI documentation generation
|
|
16
|
+
|
|
17
|
+
### Message Bus Integration
|
|
18
|
+
- Topic-based message bus for event-driven architecture
|
|
19
|
+
- Support for both worker and publisher patterns
|
|
20
|
+
- Built-in message serialization and deserialization
|
|
21
|
+
- Easy integration with AIO Pika for RabbitMQ
|
|
22
|
+
|
|
23
|
+
### Distributed WebSocket
|
|
24
|
+
- Room-based WebSocket communication
|
|
25
|
+
- Distributed broadcasting across multiple backend instances
|
|
26
|
+
- Automatic message synchronization between instances
|
|
27
|
+
- Built-in connection management and room handling
|
|
28
|
+
|
|
29
|
+
### Task Scheduling
|
|
30
|
+
- Cron-based task scheduling
|
|
31
|
+
- Support for overlapping and non-overlapping tasks
|
|
32
|
+
- Distributed task execution
|
|
33
|
+
- Easy integration with message bus for task distribution
|
|
34
|
+
|
|
35
|
+
### TypeScript Integration
|
|
36
|
+
- Automatic TypeScript interface generation
|
|
37
|
+
- Command-line tool for generating TypeScript types
|
|
38
|
+
- Support for REST endpoints, WebSocket events, and message bus payloads
|
|
39
|
+
- Type-safe frontend-backend communication
|
|
40
|
+
|
|
41
|
+
### Hexagonal Architecture
|
|
42
|
+
- Clear separation of concerns
|
|
43
|
+
- Business logic isolation from infrastructure
|
|
44
|
+
- Easy testing and maintainability
|
|
45
|
+
- Dependency injection for flexible component management
|
|
46
|
+
|
|
47
|
+
### Observability
|
|
48
|
+
- Built-in OpenTelemetry integration
|
|
49
|
+
- Distributed tracing support
|
|
50
|
+
- Logging and metrics collection
|
|
51
|
+
- Performance monitoring capabilities
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
### Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install jararaca
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Basic Usage
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from jararaca import Microservice, create_http_server
|
|
65
|
+
from jararaca.presentation.http_microservice import HttpMicroservice
|
|
66
|
+
|
|
67
|
+
# Define your microservice
|
|
68
|
+
app = Microservice(
|
|
69
|
+
providers=[
|
|
70
|
+
# Add your providers here
|
|
71
|
+
],
|
|
72
|
+
controllers=[
|
|
73
|
+
# Add your controllers here
|
|
74
|
+
],
|
|
75
|
+
interceptors=[
|
|
76
|
+
# Add your interceptors here
|
|
77
|
+
],
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Create HTTP server
|
|
81
|
+
http_app = HttpMicroservice(app)
|
|
82
|
+
web_app = create_http_server(app)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Running the Service
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Run as HTTP server
|
|
89
|
+
jararaca server app:http_app
|
|
90
|
+
|
|
91
|
+
# Run as message bus worker
|
|
92
|
+
jararaca worker app:app
|
|
93
|
+
|
|
94
|
+
# Run as scheduler
|
|
95
|
+
jararaca scheduler app:app
|
|
96
|
+
|
|
97
|
+
# Generate TypeScript interfaces
|
|
98
|
+
jararaca gen-tsi app.main:app app.ts
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Documentation
|
|
102
|
+
|
|
103
|
+
For detailed documentation, please visit our [documentation site](https://luscasleo.github.io/jararaca/).
|
|
104
|
+
|
|
105
|
+
## Examples
|
|
106
|
+
|
|
107
|
+
Check out the [examples directory](examples/) for complete working examples of:
|
|
108
|
+
- REST API implementation
|
|
109
|
+
- WebSocket usage
|
|
110
|
+
- Message bus integration
|
|
111
|
+
- Task scheduling
|
|
112
|
+
- TypeScript interface generation
|
|
113
|
+
|
|
114
|
+
## Contributing
|
|
115
|
+
|
|
116
|
+
Contributions are welcome! Please read our [contributing guidelines](.github/CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
jararaca/cli.py
CHANGED
|
@@ -7,7 +7,7 @@ import sys
|
|
|
7
7
|
import time
|
|
8
8
|
from codecs import StreamWriter
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Any
|
|
10
|
+
from typing import Any, Callable
|
|
11
11
|
from urllib.parse import parse_qs, urlparse
|
|
12
12
|
|
|
13
13
|
import aio_pika
|
|
@@ -421,25 +421,42 @@ def cli() -> None:
|
|
|
421
421
|
@click.option(
|
|
422
422
|
"--handlers",
|
|
423
423
|
type=str,
|
|
424
|
+
envvar="HANDLERS",
|
|
424
425
|
help="Comma-separated list of handler names to listen to. If not specified, all handlers will be used.",
|
|
425
426
|
)
|
|
427
|
+
@click.option(
|
|
428
|
+
"--reload",
|
|
429
|
+
is_flag=True,
|
|
430
|
+
envvar="RELOAD",
|
|
431
|
+
help="Enable auto-reload when Python files change.",
|
|
432
|
+
)
|
|
433
|
+
@click.option(
|
|
434
|
+
"--src-dir",
|
|
435
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
436
|
+
default="src",
|
|
437
|
+
envvar="SRC_DIR",
|
|
438
|
+
help="The source directory to watch for changes when --reload is enabled.",
|
|
439
|
+
)
|
|
426
440
|
def worker(
|
|
427
|
-
app_path: str,
|
|
441
|
+
app_path: str,
|
|
442
|
+
broker_url: str,
|
|
443
|
+
backend_url: str,
|
|
444
|
+
handlers: str | None,
|
|
445
|
+
reload: bool,
|
|
446
|
+
src_dir: str,
|
|
428
447
|
) -> None:
|
|
429
448
|
"""Start a message bus worker that processes asynchronous messages from a message queue."""
|
|
430
|
-
app = find_microservice_by_module_path(app_path)
|
|
431
|
-
|
|
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
449
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
450
|
+
if reload:
|
|
451
|
+
process_args = {
|
|
452
|
+
"app_path": app_path,
|
|
453
|
+
"broker_url": broker_url,
|
|
454
|
+
"backend_url": backend_url,
|
|
455
|
+
"handlers": handlers,
|
|
456
|
+
}
|
|
457
|
+
run_with_reload_watcher(process_args, run_worker_process, src_dir)
|
|
458
|
+
else:
|
|
459
|
+
run_worker_process(app_path, broker_url, backend_url, handlers)
|
|
443
460
|
|
|
444
461
|
|
|
445
462
|
@cli.command()
|
|
@@ -485,51 +502,68 @@ def server(app_path: str, host: str, port: int) -> None:
|
|
|
485
502
|
@click.argument(
|
|
486
503
|
"app_path",
|
|
487
504
|
type=str,
|
|
505
|
+
envvar="APP_PATH",
|
|
488
506
|
)
|
|
489
507
|
@click.option(
|
|
490
508
|
"--interval",
|
|
491
509
|
type=int,
|
|
492
510
|
default=1,
|
|
493
511
|
required=True,
|
|
512
|
+
envvar="INTERVAL",
|
|
494
513
|
)
|
|
495
514
|
@click.option(
|
|
496
515
|
"--broker-url",
|
|
497
516
|
type=str,
|
|
498
517
|
required=True,
|
|
518
|
+
envvar="BROKER_URL",
|
|
499
519
|
)
|
|
500
520
|
@click.option(
|
|
501
521
|
"--backend-url",
|
|
502
522
|
type=str,
|
|
503
523
|
required=True,
|
|
524
|
+
envvar="BACKEND_URL",
|
|
504
525
|
)
|
|
505
526
|
@click.option(
|
|
506
527
|
"--actions",
|
|
507
528
|
type=str,
|
|
529
|
+
envvar="ACTIONS",
|
|
508
530
|
help="Comma-separated list of action names to run (only run actions with these names)",
|
|
509
531
|
)
|
|
532
|
+
@click.option(
|
|
533
|
+
"--reload",
|
|
534
|
+
is_flag=True,
|
|
535
|
+
envvar="RELOAD",
|
|
536
|
+
help="Enable auto-reload when Python files change.",
|
|
537
|
+
)
|
|
538
|
+
@click.option(
|
|
539
|
+
"--src-dir",
|
|
540
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
541
|
+
default="src",
|
|
542
|
+
envvar="SRC_DIR",
|
|
543
|
+
help="The source directory to watch for changes when --reload is enabled.",
|
|
544
|
+
)
|
|
510
545
|
def beat(
|
|
511
546
|
interval: int,
|
|
512
547
|
broker_url: str,
|
|
513
548
|
backend_url: str,
|
|
514
549
|
app_path: str,
|
|
515
550
|
actions: str | None = None,
|
|
551
|
+
reload: bool = False,
|
|
552
|
+
src_dir: str = "src",
|
|
516
553
|
) -> None:
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
broker_url
|
|
530
|
-
scheduled_action_names=scheduler_names,
|
|
531
|
-
)
|
|
532
|
-
beat_worker.run()
|
|
554
|
+
"""Start a scheduler that dispatches scheduled actions to workers."""
|
|
555
|
+
|
|
556
|
+
if reload:
|
|
557
|
+
process_args = {
|
|
558
|
+
"app_path": app_path,
|
|
559
|
+
"interval": interval,
|
|
560
|
+
"broker_url": broker_url,
|
|
561
|
+
"backend_url": backend_url,
|
|
562
|
+
"actions": actions,
|
|
563
|
+
}
|
|
564
|
+
run_with_reload_watcher(process_args, run_beat_process, src_dir)
|
|
565
|
+
else:
|
|
566
|
+
run_beat_process(app_path, interval, broker_url, backend_url, actions)
|
|
533
567
|
|
|
534
568
|
|
|
535
569
|
def generate_interfaces(
|
|
@@ -588,29 +622,35 @@ def generate_interfaces(
|
|
|
588
622
|
@click.argument(
|
|
589
623
|
"app_path",
|
|
590
624
|
type=str,
|
|
625
|
+
envvar="APP_PATH",
|
|
591
626
|
)
|
|
592
627
|
@click.argument(
|
|
593
628
|
"file_path",
|
|
594
629
|
type=click.Path(file_okay=True, dir_okay=False),
|
|
595
630
|
required=False,
|
|
631
|
+
envvar="FILE_PATH",
|
|
596
632
|
)
|
|
597
633
|
@click.option(
|
|
598
634
|
"--watch",
|
|
599
635
|
is_flag=True,
|
|
636
|
+
envvar="WATCH",
|
|
600
637
|
)
|
|
601
638
|
@click.option(
|
|
602
639
|
"--src-dir",
|
|
603
640
|
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
604
641
|
default="src",
|
|
642
|
+
envvar="SRC_DIR",
|
|
605
643
|
)
|
|
606
644
|
@click.option(
|
|
607
645
|
"--stdout",
|
|
608
646
|
is_flag=True,
|
|
647
|
+
envvar="STDOUT",
|
|
609
648
|
help="Print generated interfaces to stdout instead of writing to a file",
|
|
610
649
|
)
|
|
611
650
|
@click.option(
|
|
612
651
|
"--post-process",
|
|
613
652
|
type=str,
|
|
653
|
+
envvar="POST_PROCESS",
|
|
614
654
|
help="Command to run after generating the interfaces, {file} will be replaced with the output file path",
|
|
615
655
|
)
|
|
616
656
|
def gen_tsi(
|
|
@@ -730,10 +770,11 @@ def camel_case_to_pascal_case(name: str) -> str:
|
|
|
730
770
|
|
|
731
771
|
|
|
732
772
|
@cli.command()
|
|
733
|
-
@click.argument("entity_name", type=click.STRING)
|
|
773
|
+
@click.argument("entity_name", type=click.STRING, envvar="ENTITY_NAME")
|
|
734
774
|
@click.argument(
|
|
735
775
|
"file_path",
|
|
736
776
|
type=click.File("w"),
|
|
777
|
+
envvar="FILE_PATH",
|
|
737
778
|
)
|
|
738
779
|
def gen_entity(entity_name: str, file_path: StreamWriter) -> None:
|
|
739
780
|
|
|
@@ -769,6 +810,7 @@ def gen_entity(entity_name: str, file_path: StreamWriter) -> None:
|
|
|
769
810
|
"--interactive-mode",
|
|
770
811
|
is_flag=True,
|
|
771
812
|
default=False,
|
|
813
|
+
envvar="INTERACTIVE_MODE",
|
|
772
814
|
help="Enable interactive mode for queue declaration (confirm before deleting existing queues)",
|
|
773
815
|
)
|
|
774
816
|
@click.option(
|
|
@@ -776,6 +818,7 @@ def gen_entity(entity_name: str, file_path: StreamWriter) -> None:
|
|
|
776
818
|
"--force",
|
|
777
819
|
is_flag=True,
|
|
778
820
|
default=False,
|
|
821
|
+
envvar="FORCE",
|
|
779
822
|
help="Force recreation by deleting existing exchanges and queues before declaring them",
|
|
780
823
|
)
|
|
781
824
|
def declare(
|
|
@@ -835,3 +878,143 @@ def declare(
|
|
|
835
878
|
raise
|
|
836
879
|
|
|
837
880
|
asyncio.run(run_declarations())
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def run_worker_process(
|
|
884
|
+
app_path: str, broker_url: str, backend_url: str, handlers: str | None
|
|
885
|
+
) -> None:
|
|
886
|
+
"""Run a worker process with the given parameters."""
|
|
887
|
+
app = find_microservice_by_module_path(app_path)
|
|
888
|
+
|
|
889
|
+
# Parse handler names if provided
|
|
890
|
+
handler_names: set[str] | None = None
|
|
891
|
+
if handlers:
|
|
892
|
+
handler_names = {name.strip() for name in handlers.split(",") if name.strip()}
|
|
893
|
+
|
|
894
|
+
click.echo(f"Starting worker for {app_path}...")
|
|
895
|
+
worker_mod.MessageBusWorker(
|
|
896
|
+
app=app,
|
|
897
|
+
broker_url=broker_url,
|
|
898
|
+
backend_url=backend_url,
|
|
899
|
+
handler_names=handler_names,
|
|
900
|
+
).start_sync()
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
def run_beat_process(
|
|
904
|
+
app_path: str, interval: int, broker_url: str, backend_url: str, actions: str | None
|
|
905
|
+
) -> None:
|
|
906
|
+
"""Run a beat scheduler process with the given parameters."""
|
|
907
|
+
app = find_microservice_by_module_path(app_path)
|
|
908
|
+
|
|
909
|
+
# Parse scheduler names if provided
|
|
910
|
+
scheduler_names: set[str] | None = None
|
|
911
|
+
if actions:
|
|
912
|
+
scheduler_names = {name.strip() for name in actions.split(",") if name.strip()}
|
|
913
|
+
|
|
914
|
+
click.echo(f"Starting beat scheduler for {app_path}...")
|
|
915
|
+
beat_worker = BeatWorker(
|
|
916
|
+
app=app,
|
|
917
|
+
interval=interval,
|
|
918
|
+
backend_url=backend_url,
|
|
919
|
+
broker_url=broker_url,
|
|
920
|
+
scheduled_action_names=scheduler_names,
|
|
921
|
+
)
|
|
922
|
+
beat_worker.run()
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
def run_with_reload_watcher(
|
|
926
|
+
process_args: dict[str, Any],
|
|
927
|
+
process_target: Callable[..., Any],
|
|
928
|
+
src_dir: str = "src",
|
|
929
|
+
) -> None:
|
|
930
|
+
"""
|
|
931
|
+
Run a process with a file watcher that will restart it when Python files change.
|
|
932
|
+
|
|
933
|
+
Args:
|
|
934
|
+
process_args: Arguments to pass to the process function
|
|
935
|
+
process_target: The function to run as the process
|
|
936
|
+
src_dir: The directory to watch for changes
|
|
937
|
+
"""
|
|
938
|
+
try:
|
|
939
|
+
from watchdog.events import FileSystemEvent, FileSystemEventHandler
|
|
940
|
+
from watchdog.observers import Observer
|
|
941
|
+
except ImportError:
|
|
942
|
+
click.echo(
|
|
943
|
+
"Watchdog is required for reload mode. Install it with: pip install watchdog",
|
|
944
|
+
file=sys.stderr,
|
|
945
|
+
)
|
|
946
|
+
return
|
|
947
|
+
|
|
948
|
+
# Run the initial process
|
|
949
|
+
process = multiprocessing.get_context("spawn").Process(
|
|
950
|
+
target=process_target,
|
|
951
|
+
kwargs=process_args,
|
|
952
|
+
daemon=False, # Non-daemon to ensure it completes properly
|
|
953
|
+
)
|
|
954
|
+
process.start() # Set up file system event handler
|
|
955
|
+
|
|
956
|
+
class PyFileChangeHandler(FileSystemEventHandler):
|
|
957
|
+
def __init__(self) -> None:
|
|
958
|
+
self.last_modified_time = time.time()
|
|
959
|
+
self.debounce_seconds = 1.0 # Debounce to avoid multiple restarts
|
|
960
|
+
self.active_process = process
|
|
961
|
+
|
|
962
|
+
def on_modified(self, event: FileSystemEvent) -> None:
|
|
963
|
+
src_path = (
|
|
964
|
+
event.src_path
|
|
965
|
+
if isinstance(event.src_path, str)
|
|
966
|
+
else str(event.src_path)
|
|
967
|
+
)
|
|
968
|
+
|
|
969
|
+
# Ignore non-Python files and directories
|
|
970
|
+
if event.is_directory or not src_path.endswith(".py"):
|
|
971
|
+
return
|
|
972
|
+
|
|
973
|
+
# Debounce to avoid multiple restarts
|
|
974
|
+
current_time = time.time()
|
|
975
|
+
if current_time - self.last_modified_time < self.debounce_seconds:
|
|
976
|
+
return
|
|
977
|
+
self.last_modified_time = current_time
|
|
978
|
+
|
|
979
|
+
click.echo(f"Detected change in {src_path}")
|
|
980
|
+
click.echo("Restarting process...")
|
|
981
|
+
|
|
982
|
+
# Terminate the current process
|
|
983
|
+
if self.active_process and self.active_process.is_alive():
|
|
984
|
+
self.active_process.terminate()
|
|
985
|
+
self.active_process.join(timeout=5)
|
|
986
|
+
|
|
987
|
+
# If process doesn't terminate, kill it
|
|
988
|
+
if self.active_process.is_alive():
|
|
989
|
+
click.echo("Process did not terminate gracefully, killing it")
|
|
990
|
+
self.active_process.kill()
|
|
991
|
+
self.active_process.join()
|
|
992
|
+
|
|
993
|
+
# Create a new process
|
|
994
|
+
self.active_process = multiprocessing.get_context("spawn").Process(
|
|
995
|
+
target=process_target,
|
|
996
|
+
kwargs=process_args,
|
|
997
|
+
daemon=False,
|
|
998
|
+
)
|
|
999
|
+
self.active_process.start()
|
|
1000
|
+
|
|
1001
|
+
# Set up observer
|
|
1002
|
+
observer = Observer()
|
|
1003
|
+
observer.schedule(PyFileChangeHandler(), src_dir, recursive=True) # type: ignore[no-untyped-call]
|
|
1004
|
+
observer.start() # type: ignore[no-untyped-call]
|
|
1005
|
+
|
|
1006
|
+
click.echo(f"Watching for changes in {os.path.abspath(src_dir)}...")
|
|
1007
|
+
try:
|
|
1008
|
+
while True:
|
|
1009
|
+
time.sleep(1)
|
|
1010
|
+
except KeyboardInterrupt:
|
|
1011
|
+
observer.stop() # type: ignore[no-untyped-call]
|
|
1012
|
+
if process.is_alive():
|
|
1013
|
+
click.echo("Stopping process...")
|
|
1014
|
+
process.terminate()
|
|
1015
|
+
process.join(timeout=5)
|
|
1016
|
+
if process.is_alive():
|
|
1017
|
+
process.kill()
|
|
1018
|
+
process.join()
|
|
1019
|
+
click.echo("Reload mode stopped")
|
|
1020
|
+
observer.join()
|