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 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, broker_url: str, backend_url: str, handlers: str | None
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
- worker_mod.MessageBusWorker(
438
- app=app,
439
- broker_url=broker_url,
440
- backend_url=backend_url,
441
- handler_names=handler_names,
442
- ).start_sync()
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
- app = find_microservice_by_module_path(app_path)
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(
526
- app=app,
527
- interval=interval,
528
- backend_url=backend_url,
529
- broker_url=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()