jararaca 0.3.11a16__tar.gz → 0.3.12a1__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 (84) hide show
  1. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/PKG-INFO +3 -3
  2. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/docs/index.md +143 -20
  3. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/docs/messagebus.md +24 -6
  4. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/docs/scheduler.md +44 -24
  5. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/docs/websocket.md +24 -10
  6. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/pyproject.toml +1 -1
  7. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/cli.py +214 -31
  8. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/tools/typescript/interface_parser.py +12 -1
  9. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/LICENSE +0 -0
  10. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/README.md +0 -0
  11. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/docs/CNAME +0 -0
  12. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/docs/architecture.md +0 -0
  13. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/docs/assets/_f04774c9-7e05-4da4-8b17-8be23f6a1475.jpeg +0 -0
  14. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/docs/assets/_f04774c9-7e05-4da4-8b17-8be23f6a1475.webp +0 -0
  15. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/docs/assets/tracing_example.png +0 -0
  16. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/docs/retry.md +0 -0
  17. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/docs/stylesheets/custom.css +0 -0
  18. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/__init__.py +0 -0
  19. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/__main__.py +0 -0
  20. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/broker_backend/__init__.py +0 -0
  21. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/broker_backend/mapper.py +0 -0
  22. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/broker_backend/redis_broker_backend.py +0 -0
  23. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/common/__init__.py +0 -0
  24. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/core/__init__.py +0 -0
  25. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/core/providers.py +0 -0
  26. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/core/uow.py +0 -0
  27. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/di.py +0 -0
  28. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/files/entity.py.mako +0 -0
  29. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/lifecycle.py +0 -0
  30. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/messagebus/__init__.py +0 -0
  31. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/messagebus/bus_message_controller.py +0 -0
  32. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/messagebus/consumers/__init__.py +0 -0
  33. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/messagebus/decorators.py +0 -0
  34. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/messagebus/interceptors/__init__.py +0 -0
  35. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +0 -0
  36. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/messagebus/interceptors/publisher_interceptor.py +0 -0
  37. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/messagebus/message.py +0 -0
  38. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/messagebus/publisher.py +0 -0
  39. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/messagebus/worker.py +0 -0
  40. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/microservice.py +0 -0
  41. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/observability/decorators.py +0 -0
  42. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/observability/interceptor.py +0 -0
  43. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/observability/providers/__init__.py +0 -0
  44. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/observability/providers/otel.py +0 -0
  45. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/persistence/base.py +0 -0
  46. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/persistence/exports.py +0 -0
  47. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/persistence/interceptors/__init__.py +0 -0
  48. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/persistence/interceptors/aiosqa_interceptor.py +0 -0
  49. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/persistence/session.py +0 -0
  50. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/persistence/sort_filter.py +0 -0
  51. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/persistence/utilities.py +0 -0
  52. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/presentation/__init__.py +0 -0
  53. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/presentation/decorators.py +0 -0
  54. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/presentation/hooks.py +0 -0
  55. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/presentation/http_microservice.py +0 -0
  56. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/presentation/server.py +0 -0
  57. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/presentation/websocket/__init__.py +0 -0
  58. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/presentation/websocket/base_types.py +0 -0
  59. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/presentation/websocket/context.py +0 -0
  60. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/presentation/websocket/decorators.py +0 -0
  61. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/presentation/websocket/redis.py +0 -0
  62. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/presentation/websocket/types.py +0 -0
  63. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/presentation/websocket/websocket_interceptor.py +0 -0
  64. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/py.typed +0 -0
  65. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/reflect/__init__.py +0 -0
  66. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/reflect/controller_inspect.py +0 -0
  67. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/reflect/metadata.py +0 -0
  68. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/rpc/__init__.py +0 -0
  69. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/rpc/http/__init__.py +0 -0
  70. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/rpc/http/backends/__init__.py +0 -0
  71. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/rpc/http/backends/httpx.py +0 -0
  72. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/rpc/http/backends/otel.py +0 -0
  73. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/rpc/http/decorators.py +0 -0
  74. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/rpc/http/httpx.py +0 -0
  75. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/scheduler/__init__.py +0 -0
  76. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/scheduler/beat_worker.py +0 -0
  77. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/scheduler/decorators.py +0 -0
  78. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/scheduler/types.py +0 -0
  79. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/tools/app_config/__init__.py +0 -0
  80. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/tools/app_config/decorators.py +0 -0
  81. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/tools/app_config/interceptor.py +0 -0
  82. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/utils/__init__.py +0 -0
  83. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/utils/rabbitmq_utils.py +0 -0
  84. {jararaca-0.3.11a16 → jararaca-0.3.12a1}/src/jararaca/utils/retry.py +0 -0
@@ -1,14 +1,14 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.1
2
2
  Name: jararaca
3
- Version: 0.3.11a16
3
+ Version: 0.3.12a1
4
4
  Summary: A simple and fast API framework for Python
5
+ Home-page: https://github.com/LuscasLeo/jararaca
5
6
  Author: Lucas S
6
7
  Author-email: me@luscasleo.dev
7
8
  Requires-Python: >=3.11,<4.0
8
9
  Classifier: Programming Language :: Python :: 3
9
10
  Classifier: Programming Language :: Python :: 3.11
10
11
  Classifier: Programming Language :: Python :: 3.12
11
- Classifier: Programming Language :: Python :: 3.13
12
12
  Provides-Extra: docs
13
13
  Provides-Extra: http
14
14
  Provides-Extra: opentelemetry
@@ -9,9 +9,9 @@ Jararaca is a powerful Python microservice framework that provides a comprehensi
9
9
  - 📦 **Dependency Injection**: Flexible dependency injection system with interceptors
10
10
  - 📊 **Database Integration**: SQLAlchemy integration with async support
11
11
  - 📡 **Message Bus**: RabbitMQ integration for event-driven architecture
12
- - 🔒 **Authentication**: Built-in JWT authentication with token blacklisting
12
+ - **Retry Mechanism**: Robust retry system with exponential backoff for resilient operations
13
13
  - 🔍 **Query Operations**: Advanced query capabilities with pagination and filtering
14
- - ⏱️ **Scheduled Tasks**: Cron-based task scheduling
14
+ - ⏱️ **Scheduled Tasks**: Distributed cron-based task scheduling with message broker integration
15
15
 
16
16
  ## Installation
17
17
 
@@ -33,9 +33,27 @@ Starts a message bus worker that processes asynchronous messages from a message
33
33
 
34
34
  **Options:**
35
35
 
36
- - `--broker-url`: The URL for the message broker (required)
37
- - `--backend-url`: The URL for the message broker backend (required)
38
- - `--handlers`: Comma-separated list of handler names to listen to (optional)
36
+ - `--broker-url`: The URL for the message broker (required) [env: BROKER_URL]
37
+ - `--backend-url`: The URL for the message broker backend (required) [env: BACKEND_URL]
38
+ - `--handlers`: Comma-separated list of handler names to listen to (optional) [env: HANDLERS]
39
+ - `--reload`: Enable auto-reload when Python files change (for development) [env: RELOAD]
40
+ - `--src-dir`: The source directory to watch for changes when --reload is enabled (default: "src") [env: SRC_DIR]
41
+
42
+ **Environment Variables:**
43
+ - `APP_PATH`: The application module path
44
+ - All options support environment variables as indicated above
45
+
46
+ **Example with environment variables:**
47
+ ```bash
48
+ export APP_PATH="app.module:app"
49
+ export BROKER_URL="amqp://guest:guest@localhost:5672/?exchange=jararaca&prefetch_count=1"
50
+ export BACKEND_URL="redis://localhost:6379"
51
+ export HANDLERS="send_email,process_payment"
52
+ export RELOAD="true"
53
+ jararaca worker
54
+ ```
55
+ - `--reload`: Enable auto-reload when Python files change (for development)
56
+ - `--src-dir`: The source directory to watch for changes when --reload is enabled (default: "src")
39
57
 
40
58
  ### `server` - HTTP Server
41
59
 
@@ -43,11 +61,39 @@ Starts a message bus worker that processes asynchronous messages from a message
43
61
  jararaca server APP_PATH [OPTIONS]
44
62
  ```
45
63
 
46
- #### Perfer `uvicorn` for production
47
-
48
64
  Starts a FastAPI HTTP server for your microservice.
49
65
 
66
+ **Options:**
67
+
68
+ - `--host`: Host to bind the server (default: "0.0.0.0") [env: HOST]
69
+ - `--port`: Port to bind the server (default: 8000) [env: PORT]
70
+
71
+ **Environment Variables:**
72
+ - `APP_PATH`: The application module path
73
+ - `HOST`: Host to bind the server
74
+ - `PORT`: Port to bind the server
75
+
76
+ **Example with environment variables:**
77
+ ```bash
78
+ export APP_PATH="app.module:app"
79
+ export HOST="127.0.0.1"
80
+ export PORT="8080"
81
+ jararaca server
82
+ ```
83
+
84
+ #### Alternative: Using `uvicorn` directly
85
+
86
+ For production environments, you can create an ASGI application and run it with uvicorn:
87
+
50
88
  ```python
89
+ from fastapi import FastAPI
90
+ from fastapi.middleware.cors import CORSMiddleware
91
+ from fastapi.types import Lifespan
92
+
93
+ from jararaca.presentation.http_microservice import HttpMicroservice
94
+ from jararaca.presentation.server import create_http_server
95
+
96
+
51
97
  def fastapi_factory(lifespan: Lifespan[FastAPI]) -> FastAPI:
52
98
  app = FastAPI(
53
99
  lifespan=lifespan,
@@ -78,26 +124,37 @@ Then run the server with:
78
124
  uvicorn app_module:asgi_app
79
125
  ```
80
126
 
81
- Starts a FastAPI HTTP server for your microservice.
82
-
83
- **Options:**
84
-
85
- - `--host`: Host to bind the server (default: "0.0.0.0")
86
- - `--port`: Port to bind the server (default: 8000)
87
-
88
- ### `scheduler` - Task Scheduler
127
+ ### `beat` - Task Scheduler
89
128
 
90
129
  ```bash
91
- jararaca scheduler APP_PATH [OPTIONS]
130
+ jararaca beat APP_PATH [OPTIONS]
92
131
  ```
93
132
 
94
133
  Runs scheduled tasks defined in your application using cron expressions.
95
134
 
96
135
  **Options:**
97
136
 
98
- - `--interval`: Polling interval in seconds (default: 1)
137
+ - `--interval`: Polling interval in seconds (default: 1) [env: INTERVAL]
138
+ - `--broker-url`: The URL for the message broker (required) [env: BROKER_URL]
139
+ - `--backend-url`: The URL for the message broker backend (required) [env: BACKEND_URL]
140
+ - `--actions`: Comma-separated list of action names to run (optional) [env: ACTIONS]
141
+ - `--reload`: Enable auto-reload when Python files change (for development) [env: RELOAD]
142
+ - `--src-dir`: The source directory to watch for changes when --reload is enabled (default: "src") [env: SRC_DIR]
99
143
 
100
- ### `scheduler_v2` - Enhanced Task Scheduler
144
+ **Environment Variables:**
145
+ - `APP_PATH`: The application module path
146
+ - All options support environment variables as indicated above
147
+
148
+ **Example with environment variables:**
149
+ ```bash
150
+ export APP_PATH="app.module:app"
151
+ export INTERVAL="5"
152
+ export BROKER_URL="amqp://guest:guest@localhost:5672/?exchange=jararaca&prefetch_count=1"
153
+ export BACKEND_URL="redis://localhost:6379"
154
+ export ACTIONS="send_emails,process_payments"
155
+ export RELOAD="true"
156
+ jararaca beat
157
+ ```
101
158
 
102
159
  ```bash
103
160
  jararaca scheduler_v2 APP_PATH [OPTIONS]
@@ -121,8 +178,14 @@ Generates TypeScript interfaces from your Python models to ensure type safety be
121
178
 
122
179
  **Options:**
123
180
 
124
- - `--watch`: Watch for file changes and regenerate TypeScript interfaces automatically
125
- - `--src-dir`: Source directory to watch for changes (default: "src")
181
+ - `--watch`: Watch for file changes and regenerate TypeScript interfaces automatically [env: WATCH]
182
+ - `--src-dir`: Source directory to watch for changes (default: "src") [env: SRC_DIR]
183
+ - `--stdout`: Print generated interfaces to stdout instead of writing to a file [env: STDOUT]
184
+ - `--post-process`: Command to run after generating the interfaces, {file} will be replaced with the output file path [env: POST_PROCESS]
185
+
186
+ **Environment Variables:**
187
+ - `APP_PATH`: The application module path
188
+ - All options support environment variables as indicated above
126
189
 
127
190
  **Example with watch mode:**
128
191
 
@@ -132,6 +195,15 @@ jararaca gen-tsi app.module:app interfaces.ts --watch
132
195
 
133
196
  This will generate the TypeScript interfaces initially and then watch for any changes to Python files in the src directory, automatically regenerating the interfaces when changes are detected. You can stop watching with Ctrl+C.
134
197
 
198
+ **Example with environment variables:**
199
+ ```bash
200
+ export APP_PATH="app.module:app"
201
+ export FILE_PATH="interfaces.ts"
202
+ export WATCH="true"
203
+ export SRC_DIR="src"
204
+ jararaca gen-tsi
205
+ ```
206
+
135
207
  **Note:** To use the watch feature, you need to install the watchdog package:
136
208
 
137
209
  ```bash
@@ -152,6 +224,57 @@ jararaca gen-entity ENTITY_NAME FILE_PATH
152
224
 
153
225
  Generates a new entity file template with proper naming conventions in different formats (snake_case, PascalCase, kebab-case).
154
226
 
227
+ **Environment Variables:**
228
+ - `ENTITY_NAME`: The name of the entity to generate
229
+ - `FILE_PATH`: The path where the entity file should be created
230
+
231
+ **Example:**
232
+
233
+ ```bash
234
+ # Using command line arguments
235
+ jararaca gen-entity User user.py
236
+
237
+ # Using environment variables
238
+ export ENTITY_NAME="User"
239
+ export FILE_PATH="user.py"
240
+ jararaca gen-entity
241
+ ```
242
+
243
+ ### `declare` - Declare Message Infrastructure
244
+
245
+ ```bash
246
+ jararaca declare APP_PATH [OPTIONS]
247
+ ```
248
+
249
+ Declares RabbitMQ infrastructure (exchanges and queues) for message handlers and schedulers without starting the actual consumption processes.
250
+
251
+ **Options:**
252
+ - `--broker-url`: Broker URL (e.g., amqp://guest:guest@localhost/) [env: BROKER_URL]
253
+ - `-i, --interactive-mode`: Enable interactive mode for queue declaration [env: INTERACTIVE_MODE]
254
+ - `-f, --force`: Force recreation by deleting existing exchanges and queues [env: FORCE]
255
+
256
+ **Environment Variables:**
257
+ - `APP_PATH`: The application module path
258
+ - `BROKER_URL`: The broker URL
259
+ - `INTERACTIVE_MODE`: Enable interactive mode
260
+ - `FORCE`: Force recreation of infrastructure
261
+
262
+ **Examples:**
263
+
264
+ ```bash
265
+ # Declare infrastructure
266
+ jararaca declare myapp:app --broker-url amqp://guest:guest@localhost/
267
+
268
+ # Force recreation of queues and exchanges
269
+ jararaca declare myapp:app --broker-url amqp://guest:guest@localhost/ --force
270
+
271
+ # Using environment variables
272
+ export APP_PATH="myapp:app"
273
+ export BROKER_URL="amqp://guest:guest@localhost/"
274
+ export FORCE="true"
275
+ jararaca declare
276
+ ```
277
+
155
278
  ## Quick Start
156
279
 
157
280
  Here's a basic example of how to create a microservice with Jararaca:
@@ -455,12 +455,30 @@ jararaca worker APP_PATH [OPTIONS]
455
455
 
456
456
  Options:
457
457
 
458
- - `--url`: AMQP URL (default: "amqp://guest:guest@localhost/")
459
- - `--username`: AMQP username (optional)
460
- - `--password`: AMQP password (optional)
461
- - `--exchange`: Exchange name (default: "jararaca_ex")
462
- - `--queue`: Queue name (default: "jararaca_q")
463
- - `--prefetch-count`: Number of messages to prefetch (default: 1)
458
+ - `--broker-url`: The URL for the message broker (required) [env: BROKER_URL]
459
+ - `--backend-url`: The URL for the message broker backend (required) [env: BACKEND_URL]
460
+ - `--handlers`: Comma-separated list of handler names to listen to (optional) [env: HANDLERS]
461
+ - `--reload`: Enable auto-reload when Python files change (for development) [env: RELOAD]
462
+ - `--src-dir`: The source directory to watch for changes when --reload is enabled (default: "src") [env: SRC_DIR]
463
+
464
+ Examples:
465
+
466
+ ```bash
467
+ # Standard worker execution
468
+ jararaca worker myapp.main:app --broker-url "amqp://guest:guest@localhost:5672/?exchange=jararaca" --backend-url "redis://localhost:6379"
469
+
470
+ # With auto-reload for development
471
+ jararaca worker myapp.main:app --broker-url "amqp://guest:guest@localhost:5672/?exchange=jararaca" --backend-url "redis://localhost:6379" --reload
472
+
473
+ # Using environment variables
474
+ export APP_PATH="myapp.main:app"
475
+ export BROKER_URL="amqp://guest:guest@localhost:5672/?exchange=jararaca"
476
+ export BACKEND_URL="redis://localhost:6379"
477
+ export RELOAD="true"
478
+ export SRC_DIR="src"
479
+ export RELOAD="true"
480
+ jararaca worker
481
+ ```
464
482
 
465
483
  ## Conclusion
466
484
 
@@ -12,22 +12,16 @@ The Jararaca scheduler allows you to:
12
12
  - Distribute scheduled tasks across multiple instances
13
13
  - Handle delayed message execution
14
14
 
15
- The scheduler has two implementations:
16
- 1. **Basic Scheduler** - Simple scheduler for local execution
17
- 2. **Enhanced Scheduler (V2)** - Distributed scheduler with improved backend support and message broker integration
15
+ The scheduler is implemented through the BeatWorker which provides distributed task scheduling via a message broker:
18
16
 
19
17
  ```mermaid
20
18
  graph TD
21
- A[Microservice] --> B[Scheduler System]
22
- B --> C[Basic Scheduler]
23
- B --> D[Enhanced Scheduler V2]
24
-
25
- C --> E[Local Task Execution]
26
- D --> F[Message Broker]
27
- F --> G[Workers]
28
- D --> H[Backend Store]
29
- H --> I[Last Execution Time]
30
- H --> J[Delayed Messages]
19
+ A[Microservice] --> B[BeatWorker]
20
+ B --> C[Message Broker]
21
+ B --> D[Backend Store]
22
+ C --> E[Message Processing]
23
+ D --> F[Last Execution Time]
24
+ D --> G[Delayed Messages]
31
25
  ```
32
26
 
33
27
  ## Using the Scheduler
@@ -73,31 +67,57 @@ Jararaca uses standard cron expressions for scheduling. Here are some examples:
73
67
  - `0 0 * * 0` - Run at midnight every Sunday
74
68
  - `0 0 1 * *` - Run at midnight on the first day of every month
75
69
 
76
- ## Basic Scheduler Implementation
70
+ ## Using the BeatWorker Scheduler
77
71
 
78
- The basic scheduler is suitable for simpler applications where tasks run locally:
72
+ The BeatWorker scheduler provides distributed task execution through a message broker:
79
73
 
80
74
  ```python
81
75
  from jararaca import Microservice, ScheduledAction
76
+ from jararaca.scheduler.beat_worker import BeatWorker
82
77
 
83
78
  app = Microservice(
84
79
  # Your microservice configuration
85
80
  )
86
81
 
87
82
  # Run the scheduler
88
- from jararaca.scheduler.scheduler import Scheduler
89
-
90
- scheduler = Scheduler(app, interval=1)
91
- scheduler.run()
83
+ beat_worker = BeatWorker(
84
+ app=app,
85
+ interval=1,
86
+ backend_url="redis://localhost:6379",
87
+ broker_url="amqp://guest:guest@localhost:5672/?exchange=jararaca",
88
+ scheduled_action_names=None # Optional set of action names to run
89
+ )
90
+ beat_worker.run()
92
91
  ```
93
92
 
94
- ## Enhanced Scheduler V2 Implementation
93
+ You can also use the CLI command to run the scheduler:
95
94
 
96
- The V2 scheduler adds support for distributed execution and message broker integration, making it ideal for more complex applications:
95
+ ```bash
96
+ # Standard beat scheduler execution
97
+ jararaca beat app_module:app --interval 1 --broker-url "amqp://guest:guest@localhost:5672/?exchange=jararaca" --backend-url "redis://localhost:6379"
98
+
99
+ # With auto-reload for development (automatically restarts when Python files change)
100
+ jararaca beat app_module:app --interval 1 --broker-url "amqp://guest:guest@localhost:5672/?exchange=jararaca" --backend-url "redis://localhost:6379" --reload
101
+
102
+ # Using environment variables
103
+ export APP_PATH="app_module:app"
104
+ export INTERVAL="1"
105
+ export BROKER_URL="amqp://guest:guest@localhost:5672/?exchange=jararaca"
106
+ export BACKEND_URL="redis://localhost:6379"
107
+ export RELOAD="true"
108
+ export SRC_DIR="src"
109
+ jararaca beat
110
+ ```
97
111
 
98
- ```python
99
- from jararaca import Microservice, ScheduledAction
100
- from jararaca.scheduler.scheduler_v2 import SchedulerV2
112
+ All command options support environment variables:
113
+ - `APP_PATH`: The application module path [required]
114
+ - `INTERVAL`: Polling interval in seconds [default: 1]
115
+ - `BROKER_URL`: The URL for the message broker [required]
116
+ - `BACKEND_URL`: The URL for the message broker backend [required]
117
+ - `ACTIONS`: Comma-separated list of action names to run [optional]
118
+ - `RELOAD`: Enable auto-reload when Python files change [optional]
119
+ - `SRC_DIR`: The source directory to watch for changes when using reload [default: "src"]
120
+ ```
101
121
 
102
122
  app = Microservice(
103
123
  # Your microservice configuration
@@ -57,24 +57,38 @@ class WebSocketMessage(WebSocketMessageBase):
57
57
 
58
58
  ### 3. WebSocketConnectionManager
59
59
 
60
- Manages WebSocket connections, rooms, and message distribution:
60
+ A Protocol that defines the interface for managing WebSocket connections:
61
61
 
62
+ ```python
63
+ class WebSocketConnectionManager(Protocol):
64
+ async def broadcast(self, message: bytes) -> None: ...
65
+ async def send(self, rooms: list[str], message: WebSocketMessageBase) -> None: ...
66
+ async def join(self, rooms: list[str], websocket: WebSocket) -> None: ...
67
+ async def add_websocket(self, websocket: WebSocket) -> None: ...
68
+ async def remove_websocket(self, websocket: WebSocket) -> None: ...
69
+ ```
70
+
71
+ The WebSocketConnectionManager:
62
72
  - Maintains a registry of active WebSocket connections
63
73
  - Groups connections into named rooms
64
74
  - Provides methods for broadcasting and sending targeted messages
65
75
 
66
- ### 4. WebSocketConnectionBackend
76
+ ### 4. Context-based WebSocket Access
67
77
 
68
- A protocol defining the contract for backend implementations:
78
+ Jararaca uses context variables to provide access to WebSocket functionality anywhere in your application:
69
79
 
70
80
  ```python
71
- class WebSocketConnectionBackend(Protocol):
72
- async def broadcast(self, message: bytes) -> None: ...
73
- async def send(self, rooms: list[str], message: bytes) -> None: ...
74
- def configure(
75
- self, broadcast: BroadcastFunc, send: SendFunc, shutdown_event: asyncio.Event
76
- ) -> None: ...
77
- async def shutdown(self) -> None: ...
81
+ from jararaca.presentation.websocket.context import use_ws_manager, use_ws_message_sender
82
+
83
+ # Send a message to specific rooms
84
+ async def notify_users(message_data: dict, room_id: str):
85
+ message = UserNotificationMessage(**message_data)
86
+ await use_ws_message_sender().send([room_id], message)
87
+
88
+ # Or directly from a WebSocketMessage instance
89
+ async def send_update(update_data: dict, room_id: str):
90
+ message = SystemUpdateMessage(**update_data)
91
+ await message.send(room_id)
78
92
  ```
79
93
 
80
94
  ### 5. RedisWebSocketConnectionBackend
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "jararaca"
3
- version = "0.3.11a16"
3
+ version = "0.3.12a1"
4
4
  description = "A simple and fast API framework for Python"
5
5
  authors = ["Lucas S <me@luscasleo.dev>"]
6
6
  readme = "README.md"
@@ -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()
@@ -406,6 +406,7 @@ export type ResponseType =
406
406
  export interface HttpBackendRequest {
407
407
  method: string;
408
408
  path: string;
409
+ pathParams: { [key: string]: string };
409
410
  headers: { [key: string]: string };
410
411
  query: { [key: string]: unknown };
411
412
  body: unknown;
@@ -543,6 +544,16 @@ def write_rest_controller_to_typescript_interface(
543
544
  )
544
545
  class_buffer.write(f"\t\t\tpath: `/{final_path}`,\n")
545
546
 
547
+ # Sort path params
548
+ path_params = sorted(
549
+ [param for param in arg_params_spec if param.type_ == "path"],
550
+ key=lambda x: x.name,
551
+ )
552
+ class_buffer.write("\t\t\tpathParams: {\n")
553
+ for param in path_params:
554
+ class_buffer.write(f'\t\t\t\t"{param.name}": {param.name},\n')
555
+ class_buffer.write("\t\t\t},\n")
556
+
546
557
  # Sort headers
547
558
  header_params = sorted(
548
559
  [param for param in arg_params_spec if param.type_ == "header"],
@@ -593,7 +604,7 @@ class HttpParemeterSpec:
593
604
 
594
605
  def parse_path_with_params(path: str, parameters: list[HttpParemeterSpec]) -> str:
595
606
  for parameter in parameters:
596
- path = path.replace(f"{{{parameter.name}}}", f"${{{parameter.name}}}")
607
+ path = path.replace(f"{{{parameter.name}}}", f":{parameter.name}")
597
608
  return path
598
609
 
599
610
 
File without changes
File without changes
File without changes
File without changes