jararaca 0.3.11a16__py3-none-any.whl → 0.3.12__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/__init__.py +106 -8
- jararaca/cli.py +216 -31
- jararaca/messagebus/worker.py +749 -272
- jararaca/microservice.py +42 -0
- jararaca/persistence/interceptors/aiosqa_interceptor.py +82 -73
- jararaca/persistence/interceptors/constants.py +1 -0
- jararaca/persistence/interceptors/decorators.py +45 -0
- jararaca/presentation/server.py +57 -11
- jararaca/presentation/websocket/redis.py +113 -7
- jararaca/reflect/metadata.py +1 -1
- jararaca/rpc/http/__init__.py +97 -0
- jararaca/rpc/http/backends/__init__.py +10 -0
- jararaca/rpc/http/backends/httpx.py +39 -9
- jararaca/rpc/http/decorators.py +302 -6
- jararaca/scheduler/beat_worker.py +550 -91
- jararaca/tools/typescript/__init__.py +0 -0
- jararaca/tools/typescript/decorators.py +95 -0
- jararaca/tools/typescript/interface_parser.py +699 -156
- jararaca-0.3.12.dist-info/LICENSE +674 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.3.12.dist-info}/METADATA +4 -3
- {jararaca-0.3.11a16.dist-info → jararaca-0.3.12.dist-info}/RECORD +26 -19
- {jararaca-0.3.11a16.dist-info → jararaca-0.3.12.dist-info}/WHEEL +1 -1
- pyproject.toml +86 -0
- /jararaca-0.3.11a16.dist-info/LICENSE → /LICENSE +0 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.3.12.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/__init__.py
CHANGED
|
@@ -45,26 +45,47 @@ if TYPE_CHECKING:
|
|
|
45
45
|
)
|
|
46
46
|
from jararaca.rpc.http.backends.httpx import HTTPXHttpRPCAsyncBackend
|
|
47
47
|
from jararaca.rpc.http.backends.otel import TracedRequestMiddleware
|
|
48
|
-
from jararaca.rpc.http.decorators import
|
|
48
|
+
from jararaca.rpc.http.decorators import ( # New request parameter decorators; Configuration decorators; Authentication classes; Middleware classes; Configuration classes; Exception classes
|
|
49
|
+
ApiKeyAuth,
|
|
50
|
+
AuthenticationMiddleware,
|
|
51
|
+
BasicAuth,
|
|
52
|
+
BearerTokenAuth,
|
|
53
|
+
Body,
|
|
54
|
+
CacheMiddleware,
|
|
55
|
+
ContentType,
|
|
56
|
+
)
|
|
49
57
|
from jararaca.rpc.http.decorators import Delete as HttpDelete
|
|
58
|
+
from jararaca.rpc.http.decorators import ( # New request parameter decorators; Configuration decorators; Authentication classes; Middleware classes; Configuration classes; Exception classes
|
|
59
|
+
File,
|
|
60
|
+
FormData,
|
|
61
|
+
)
|
|
50
62
|
from jararaca.rpc.http.decorators import Get as HttpGet
|
|
51
|
-
from jararaca.rpc.http.decorators import (
|
|
63
|
+
from jararaca.rpc.http.decorators import ( # New request parameter decorators; Configuration decorators; Authentication classes; Middleware classes; Configuration classes; Exception classes
|
|
52
64
|
GlobalHttpErrorHandler,
|
|
53
65
|
Header,
|
|
54
66
|
HttpMapping,
|
|
55
67
|
HttpRpcClientBuilder,
|
|
56
68
|
)
|
|
57
69
|
from jararaca.rpc.http.decorators import Patch as HttpPatch
|
|
58
|
-
from jararaca.rpc.http.decorators import
|
|
70
|
+
from jararaca.rpc.http.decorators import ( # New request parameter decorators; Configuration decorators; Authentication classes; Middleware classes; Configuration classes; Exception classes
|
|
71
|
+
PathParam,
|
|
72
|
+
)
|
|
59
73
|
from jararaca.rpc.http.decorators import Post as HttpPost
|
|
60
74
|
from jararaca.rpc.http.decorators import Put as HttpPut
|
|
61
|
-
from jararaca.rpc.http.decorators import (
|
|
75
|
+
from jararaca.rpc.http.decorators import ( # New request parameter decorators; Configuration decorators; Authentication classes; Middleware classes; Configuration classes; Exception classes
|
|
62
76
|
Query,
|
|
63
77
|
RequestAttribute,
|
|
78
|
+
RequestHook,
|
|
79
|
+
ResponseHook,
|
|
80
|
+
ResponseMiddleware,
|
|
64
81
|
RestClient,
|
|
82
|
+
Retry,
|
|
83
|
+
RetryConfig,
|
|
65
84
|
RouteHttpErrorHandler,
|
|
66
85
|
RPCRequestNetworkError,
|
|
67
86
|
RPCUnhandleError,
|
|
87
|
+
Timeout,
|
|
88
|
+
TimeoutException,
|
|
68
89
|
)
|
|
69
90
|
|
|
70
91
|
from .core.providers import ProviderSpec, Token
|
|
@@ -81,6 +102,8 @@ if TYPE_CHECKING:
|
|
|
81
102
|
from .messagebus.publisher import use_publisher
|
|
82
103
|
from .microservice import (
|
|
83
104
|
Microservice,
|
|
105
|
+
is_shutting_down,
|
|
106
|
+
request_shutdown,
|
|
84
107
|
use_app_context,
|
|
85
108
|
use_app_transaction_context,
|
|
86
109
|
use_app_tx_ctx_data,
|
|
@@ -96,6 +119,11 @@ if TYPE_CHECKING:
|
|
|
96
119
|
use_session,
|
|
97
120
|
use_transaction,
|
|
98
121
|
)
|
|
122
|
+
from .persistence.interceptors.decorators import (
|
|
123
|
+
set_use_persistence_session,
|
|
124
|
+
skip_persistence_session,
|
|
125
|
+
uses_persistence_session,
|
|
126
|
+
)
|
|
99
127
|
from .persistence.utilities import (
|
|
100
128
|
CriteriaBasedAttributeQueryInjector,
|
|
101
129
|
CRUDOperations,
|
|
@@ -137,6 +165,11 @@ if TYPE_CHECKING:
|
|
|
137
165
|
from .presentation.websocket.websocket_interceptor import WebSocketInterceptor
|
|
138
166
|
from .scheduler.decorators import ScheduledAction
|
|
139
167
|
from .tools.app_config.interceptor import AppConfigurationInterceptor
|
|
168
|
+
from .tools.typescript.decorators import (
|
|
169
|
+
MutationEndpoint,
|
|
170
|
+
QueryEndpoint,
|
|
171
|
+
SplitInputOutput,
|
|
172
|
+
)
|
|
140
173
|
|
|
141
174
|
__all__ = [
|
|
142
175
|
"SetMetadata",
|
|
@@ -186,6 +219,12 @@ if TYPE_CHECKING:
|
|
|
186
219
|
"QueryInjector",
|
|
187
220
|
"HttpMicroservice",
|
|
188
221
|
"use_current_container",
|
|
222
|
+
"use_app_context",
|
|
223
|
+
"use_app_transaction_context",
|
|
224
|
+
"use_app_tx_ctx_data",
|
|
225
|
+
"is_shutting_down",
|
|
226
|
+
"request_shutdown",
|
|
227
|
+
"Microservice",
|
|
189
228
|
"T_BASEMODEL",
|
|
190
229
|
"DatedEntity",
|
|
191
230
|
"BaseEntity",
|
|
@@ -208,7 +247,6 @@ if TYPE_CHECKING:
|
|
|
208
247
|
"MessageBusController",
|
|
209
248
|
"MessageHandler",
|
|
210
249
|
"ScheduledAction",
|
|
211
|
-
"Microservice",
|
|
212
250
|
"ProviderSpec",
|
|
213
251
|
"Token",
|
|
214
252
|
"AIOSqlAlchemySessionInterceptor",
|
|
@@ -220,6 +258,9 @@ if TYPE_CHECKING:
|
|
|
220
258
|
"use_transaction",
|
|
221
259
|
"providing_session",
|
|
222
260
|
"provide_session",
|
|
261
|
+
"uses_persistence_session",
|
|
262
|
+
"skip_persistence_session",
|
|
263
|
+
"set_use_persistence_session",
|
|
223
264
|
"providing_transaction",
|
|
224
265
|
"providing_new_session",
|
|
225
266
|
"Post",
|
|
@@ -233,6 +274,9 @@ if TYPE_CHECKING:
|
|
|
233
274
|
"MessageBusPublisherInterceptor",
|
|
234
275
|
"RedisWebSocketConnectionBackend",
|
|
235
276
|
"AppConfigurationInterceptor",
|
|
277
|
+
"QueryEndpoint",
|
|
278
|
+
"MutationEndpoint",
|
|
279
|
+
"SplitInputOutput",
|
|
236
280
|
"UseMiddleware",
|
|
237
281
|
"UseDependency",
|
|
238
282
|
"GlobalHttpErrorHandler",
|
|
@@ -242,9 +286,27 @@ if TYPE_CHECKING:
|
|
|
242
286
|
"provide_ws_manager",
|
|
243
287
|
"HttpRpcClientBuilder",
|
|
244
288
|
"HTTPXHttpRPCAsyncBackend",
|
|
245
|
-
|
|
246
|
-
"
|
|
247
|
-
"
|
|
289
|
+
# New request parameter decorators
|
|
290
|
+
"FormData",
|
|
291
|
+
"File",
|
|
292
|
+
# Configuration decorators
|
|
293
|
+
"Timeout",
|
|
294
|
+
"Retry",
|
|
295
|
+
"ContentType",
|
|
296
|
+
# Authentication classes
|
|
297
|
+
"BearerTokenAuth",
|
|
298
|
+
"BasicAuth",
|
|
299
|
+
"ApiKeyAuth",
|
|
300
|
+
# Middleware classes
|
|
301
|
+
"CacheMiddleware",
|
|
302
|
+
"AuthenticationMiddleware",
|
|
303
|
+
"ResponseMiddleware",
|
|
304
|
+
"RequestHook",
|
|
305
|
+
"ResponseHook",
|
|
306
|
+
# Configuration classes
|
|
307
|
+
"RetryConfig",
|
|
308
|
+
# Exception classes
|
|
309
|
+
"TimeoutException",
|
|
248
310
|
"AppTransactionContext",
|
|
249
311
|
"AppContext",
|
|
250
312
|
"ControllerMemberReflect",
|
|
@@ -401,6 +463,21 @@ _dynamic_imports: "dict[str, tuple[str, str, str | None]]" = {
|
|
|
401
463
|
"persistence.interceptors.aiosqa_interceptor",
|
|
402
464
|
None,
|
|
403
465
|
),
|
|
466
|
+
"uses_persistence_session": (
|
|
467
|
+
__SPEC_PARENT__,
|
|
468
|
+
"persistence.interceptors.decorators",
|
|
469
|
+
None,
|
|
470
|
+
),
|
|
471
|
+
"skip_persistence_session": (
|
|
472
|
+
__SPEC_PARENT__,
|
|
473
|
+
"persistence.interceptors.decorators",
|
|
474
|
+
None,
|
|
475
|
+
),
|
|
476
|
+
"set_use_persistence_session": (
|
|
477
|
+
__SPEC_PARENT__,
|
|
478
|
+
"persistence.interceptors.decorators",
|
|
479
|
+
None,
|
|
480
|
+
),
|
|
404
481
|
"Post": (__SPEC_PARENT__, "presentation.decorators", None),
|
|
405
482
|
"Get": (__SPEC_PARENT__, "presentation.decorators", None),
|
|
406
483
|
"Patch": (__SPEC_PARENT__, "presentation.decorators", None),
|
|
@@ -427,6 +504,9 @@ _dynamic_imports: "dict[str, tuple[str, str, str | None]]" = {
|
|
|
427
504
|
"tools.app_config.interceptor",
|
|
428
505
|
None,
|
|
429
506
|
),
|
|
507
|
+
"QueryEndpoint": (__SPEC_PARENT__, "tools.typescript.decorators", None),
|
|
508
|
+
"MutationEndpoint": (__SPEC_PARENT__, "tools.typescript.decorators", None),
|
|
509
|
+
"SplitInputOutput": (__SPEC_PARENT__, "tools.typescript.decorators", None),
|
|
430
510
|
"UseMiddleware": (__SPEC_PARENT__, "presentation.decorators", None),
|
|
431
511
|
"UseDependency": (__SPEC_PARENT__, "presentation.decorators", None),
|
|
432
512
|
"GlobalHttpErrorHandler": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
@@ -440,9 +520,27 @@ _dynamic_imports: "dict[str, tuple[str, str, str | None]]" = {
|
|
|
440
520
|
"provide_ws_manager": (__SPEC_PARENT__, "presentation.websocket.context", None),
|
|
441
521
|
"HttpRpcClientBuilder": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
442
522
|
"HTTPXHttpRPCAsyncBackend": (__SPEC_PARENT__, "rpc.http.backends.httpx", None),
|
|
523
|
+
# New HTTP RPC classes
|
|
524
|
+
"FormData": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
525
|
+
"File": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
526
|
+
"Timeout": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
527
|
+
"Retry": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
528
|
+
"ContentType": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
529
|
+
"BearerTokenAuth": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
530
|
+
"BasicAuth": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
531
|
+
"ApiKeyAuth": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
532
|
+
"CacheMiddleware": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
533
|
+
"AuthenticationMiddleware": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
534
|
+
"ResponseMiddleware": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
535
|
+
"RequestHook": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
536
|
+
"ResponseHook": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
537
|
+
"RetryConfig": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
538
|
+
"TimeoutException": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
443
539
|
"use_app_context": (__SPEC_PARENT__, "microservice", None),
|
|
444
540
|
"use_app_transaction_context": (__SPEC_PARENT__, "microservice", None),
|
|
445
541
|
"use_app_tx_ctx_data": (__SPEC_PARENT__, "microservice", None),
|
|
542
|
+
"is_shutting_down": (__SPEC_PARENT__, "microservice", None),
|
|
543
|
+
"request_shutdown": (__SPEC_PARENT__, "microservice", None),
|
|
446
544
|
"AppContext": (__SPEC_PARENT__, "microservice", None),
|
|
447
545
|
"AppInterceptor": (__SPEC_PARENT__, "microservice", None),
|
|
448
546
|
"AppTransactionContext": (__SPEC_PARENT__, "microservice", None),
|
jararaca/cli.py
CHANGED
|
@@ -5,9 +5,10 @@ import multiprocessing
|
|
|
5
5
|
import os
|
|
6
6
|
import sys
|
|
7
7
|
import time
|
|
8
|
+
import traceback
|
|
8
9
|
from codecs import StreamWriter
|
|
9
10
|
from pathlib import Path
|
|
10
|
-
from typing import Any
|
|
11
|
+
from typing import Any, Callable
|
|
11
12
|
from urllib.parse import parse_qs, urlparse
|
|
12
13
|
|
|
13
14
|
import aio_pika
|
|
@@ -421,25 +422,42 @@ def cli() -> None:
|
|
|
421
422
|
@click.option(
|
|
422
423
|
"--handlers",
|
|
423
424
|
type=str,
|
|
425
|
+
envvar="HANDLERS",
|
|
424
426
|
help="Comma-separated list of handler names to listen to. If not specified, all handlers will be used.",
|
|
425
427
|
)
|
|
428
|
+
@click.option(
|
|
429
|
+
"--reload",
|
|
430
|
+
is_flag=True,
|
|
431
|
+
envvar="RELOAD",
|
|
432
|
+
help="Enable auto-reload when Python files change.",
|
|
433
|
+
)
|
|
434
|
+
@click.option(
|
|
435
|
+
"--src-dir",
|
|
436
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
437
|
+
default="src",
|
|
438
|
+
envvar="SRC_DIR",
|
|
439
|
+
help="The source directory to watch for changes when --reload is enabled.",
|
|
440
|
+
)
|
|
426
441
|
def worker(
|
|
427
|
-
app_path: str,
|
|
442
|
+
app_path: str,
|
|
443
|
+
broker_url: str,
|
|
444
|
+
backend_url: str,
|
|
445
|
+
handlers: str | None,
|
|
446
|
+
reload: bool,
|
|
447
|
+
src_dir: str,
|
|
428
448
|
) -> None:
|
|
429
449
|
"""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
450
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
451
|
+
if reload:
|
|
452
|
+
process_args = {
|
|
453
|
+
"app_path": app_path,
|
|
454
|
+
"broker_url": broker_url,
|
|
455
|
+
"backend_url": backend_url,
|
|
456
|
+
"handlers": handlers,
|
|
457
|
+
}
|
|
458
|
+
run_with_reload_watcher(process_args, run_worker_process, src_dir)
|
|
459
|
+
else:
|
|
460
|
+
run_worker_process(app_path, broker_url, backend_url, handlers)
|
|
443
461
|
|
|
444
462
|
|
|
445
463
|
@cli.command()
|
|
@@ -485,51 +503,68 @@ def server(app_path: str, host: str, port: int) -> None:
|
|
|
485
503
|
@click.argument(
|
|
486
504
|
"app_path",
|
|
487
505
|
type=str,
|
|
506
|
+
envvar="APP_PATH",
|
|
488
507
|
)
|
|
489
508
|
@click.option(
|
|
490
509
|
"--interval",
|
|
491
510
|
type=int,
|
|
492
511
|
default=1,
|
|
493
512
|
required=True,
|
|
513
|
+
envvar="INTERVAL",
|
|
494
514
|
)
|
|
495
515
|
@click.option(
|
|
496
516
|
"--broker-url",
|
|
497
517
|
type=str,
|
|
498
518
|
required=True,
|
|
519
|
+
envvar="BROKER_URL",
|
|
499
520
|
)
|
|
500
521
|
@click.option(
|
|
501
522
|
"--backend-url",
|
|
502
523
|
type=str,
|
|
503
524
|
required=True,
|
|
525
|
+
envvar="BACKEND_URL",
|
|
504
526
|
)
|
|
505
527
|
@click.option(
|
|
506
528
|
"--actions",
|
|
507
529
|
type=str,
|
|
530
|
+
envvar="ACTIONS",
|
|
508
531
|
help="Comma-separated list of action names to run (only run actions with these names)",
|
|
509
532
|
)
|
|
533
|
+
@click.option(
|
|
534
|
+
"--reload",
|
|
535
|
+
is_flag=True,
|
|
536
|
+
envvar="RELOAD",
|
|
537
|
+
help="Enable auto-reload when Python files change.",
|
|
538
|
+
)
|
|
539
|
+
@click.option(
|
|
540
|
+
"--src-dir",
|
|
541
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
542
|
+
default="src",
|
|
543
|
+
envvar="SRC_DIR",
|
|
544
|
+
help="The source directory to watch for changes when --reload is enabled.",
|
|
545
|
+
)
|
|
510
546
|
def beat(
|
|
511
547
|
interval: int,
|
|
512
548
|
broker_url: str,
|
|
513
549
|
backend_url: str,
|
|
514
550
|
app_path: str,
|
|
515
551
|
actions: str | None = None,
|
|
552
|
+
reload: bool = False,
|
|
553
|
+
src_dir: str = "src",
|
|
516
554
|
) -> 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()
|
|
555
|
+
"""Start a scheduler that dispatches scheduled actions to workers."""
|
|
556
|
+
|
|
557
|
+
if reload:
|
|
558
|
+
process_args = {
|
|
559
|
+
"app_path": app_path,
|
|
560
|
+
"interval": interval,
|
|
561
|
+
"broker_url": broker_url,
|
|
562
|
+
"backend_url": backend_url,
|
|
563
|
+
"actions": actions,
|
|
564
|
+
}
|
|
565
|
+
run_with_reload_watcher(process_args, run_beat_process, src_dir)
|
|
566
|
+
else:
|
|
567
|
+
run_beat_process(app_path, interval, broker_url, backend_url, actions)
|
|
533
568
|
|
|
534
569
|
|
|
535
570
|
def generate_interfaces(
|
|
@@ -581,6 +616,7 @@ def generate_interfaces(
|
|
|
581
616
|
return content
|
|
582
617
|
except Exception as e:
|
|
583
618
|
click.echo(f"Error generating TypeScript interfaces: {e}", file=sys.stderr)
|
|
619
|
+
traceback.print_exc(file=sys.stderr)
|
|
584
620
|
return ""
|
|
585
621
|
|
|
586
622
|
|
|
@@ -588,29 +624,35 @@ def generate_interfaces(
|
|
|
588
624
|
@click.argument(
|
|
589
625
|
"app_path",
|
|
590
626
|
type=str,
|
|
627
|
+
envvar="APP_PATH",
|
|
591
628
|
)
|
|
592
629
|
@click.argument(
|
|
593
630
|
"file_path",
|
|
594
631
|
type=click.Path(file_okay=True, dir_okay=False),
|
|
595
632
|
required=False,
|
|
633
|
+
envvar="FILE_PATH",
|
|
596
634
|
)
|
|
597
635
|
@click.option(
|
|
598
636
|
"--watch",
|
|
599
637
|
is_flag=True,
|
|
638
|
+
envvar="WATCH",
|
|
600
639
|
)
|
|
601
640
|
@click.option(
|
|
602
641
|
"--src-dir",
|
|
603
642
|
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
604
643
|
default="src",
|
|
644
|
+
envvar="SRC_DIR",
|
|
605
645
|
)
|
|
606
646
|
@click.option(
|
|
607
647
|
"--stdout",
|
|
608
648
|
is_flag=True,
|
|
649
|
+
envvar="STDOUT",
|
|
609
650
|
help="Print generated interfaces to stdout instead of writing to a file",
|
|
610
651
|
)
|
|
611
652
|
@click.option(
|
|
612
653
|
"--post-process",
|
|
613
654
|
type=str,
|
|
655
|
+
envvar="POST_PROCESS",
|
|
614
656
|
help="Command to run after generating the interfaces, {file} will be replaced with the output file path",
|
|
615
657
|
)
|
|
616
658
|
def gen_tsi(
|
|
@@ -730,10 +772,11 @@ def camel_case_to_pascal_case(name: str) -> str:
|
|
|
730
772
|
|
|
731
773
|
|
|
732
774
|
@cli.command()
|
|
733
|
-
@click.argument("entity_name", type=click.STRING)
|
|
775
|
+
@click.argument("entity_name", type=click.STRING, envvar="ENTITY_NAME")
|
|
734
776
|
@click.argument(
|
|
735
777
|
"file_path",
|
|
736
778
|
type=click.File("w"),
|
|
779
|
+
envvar="FILE_PATH",
|
|
737
780
|
)
|
|
738
781
|
def gen_entity(entity_name: str, file_path: StreamWriter) -> None:
|
|
739
782
|
|
|
@@ -769,6 +812,7 @@ def gen_entity(entity_name: str, file_path: StreamWriter) -> None:
|
|
|
769
812
|
"--interactive-mode",
|
|
770
813
|
is_flag=True,
|
|
771
814
|
default=False,
|
|
815
|
+
envvar="INTERACTIVE_MODE",
|
|
772
816
|
help="Enable interactive mode for queue declaration (confirm before deleting existing queues)",
|
|
773
817
|
)
|
|
774
818
|
@click.option(
|
|
@@ -776,6 +820,7 @@ def gen_entity(entity_name: str, file_path: StreamWriter) -> None:
|
|
|
776
820
|
"--force",
|
|
777
821
|
is_flag=True,
|
|
778
822
|
default=False,
|
|
823
|
+
envvar="FORCE",
|
|
779
824
|
help="Force recreation by deleting existing exchanges and queues before declaring them",
|
|
780
825
|
)
|
|
781
826
|
def declare(
|
|
@@ -835,3 +880,143 @@ def declare(
|
|
|
835
880
|
raise
|
|
836
881
|
|
|
837
882
|
asyncio.run(run_declarations())
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def run_worker_process(
|
|
886
|
+
app_path: str, broker_url: str, backend_url: str, handlers: str | None
|
|
887
|
+
) -> None:
|
|
888
|
+
"""Run a worker process with the given parameters."""
|
|
889
|
+
app = find_microservice_by_module_path(app_path)
|
|
890
|
+
|
|
891
|
+
# Parse handler names if provided
|
|
892
|
+
handler_names: set[str] | None = None
|
|
893
|
+
if handlers:
|
|
894
|
+
handler_names = {name.strip() for name in handlers.split(",") if name.strip()}
|
|
895
|
+
|
|
896
|
+
click.echo(f"Starting worker for {app_path}...")
|
|
897
|
+
worker_mod.MessageBusWorker(
|
|
898
|
+
app=app,
|
|
899
|
+
broker_url=broker_url,
|
|
900
|
+
backend_url=backend_url,
|
|
901
|
+
handler_names=handler_names,
|
|
902
|
+
).start_sync()
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def run_beat_process(
|
|
906
|
+
app_path: str, interval: int, broker_url: str, backend_url: str, actions: str | None
|
|
907
|
+
) -> None:
|
|
908
|
+
"""Run a beat scheduler process with the given parameters."""
|
|
909
|
+
app = find_microservice_by_module_path(app_path)
|
|
910
|
+
|
|
911
|
+
# Parse scheduler names if provided
|
|
912
|
+
scheduler_names: set[str] | None = None
|
|
913
|
+
if actions:
|
|
914
|
+
scheduler_names = {name.strip() for name in actions.split(",") if name.strip()}
|
|
915
|
+
|
|
916
|
+
click.echo(f"Starting beat scheduler for {app_path}...")
|
|
917
|
+
beat_worker = BeatWorker(
|
|
918
|
+
app=app,
|
|
919
|
+
interval=interval,
|
|
920
|
+
backend_url=backend_url,
|
|
921
|
+
broker_url=broker_url,
|
|
922
|
+
scheduled_action_names=scheduler_names,
|
|
923
|
+
)
|
|
924
|
+
beat_worker.run()
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
def run_with_reload_watcher(
|
|
928
|
+
process_args: dict[str, Any],
|
|
929
|
+
process_target: Callable[..., Any],
|
|
930
|
+
src_dir: str = "src",
|
|
931
|
+
) -> None:
|
|
932
|
+
"""
|
|
933
|
+
Run a process with a file watcher that will restart it when Python files change.
|
|
934
|
+
|
|
935
|
+
Args:
|
|
936
|
+
process_args: Arguments to pass to the process function
|
|
937
|
+
process_target: The function to run as the process
|
|
938
|
+
src_dir: The directory to watch for changes
|
|
939
|
+
"""
|
|
940
|
+
try:
|
|
941
|
+
from watchdog.events import FileSystemEvent, FileSystemEventHandler
|
|
942
|
+
from watchdog.observers import Observer
|
|
943
|
+
except ImportError:
|
|
944
|
+
click.echo(
|
|
945
|
+
"Watchdog is required for reload mode. Install it with: pip install watchdog",
|
|
946
|
+
file=sys.stderr,
|
|
947
|
+
)
|
|
948
|
+
return
|
|
949
|
+
|
|
950
|
+
# Run the initial process
|
|
951
|
+
process = multiprocessing.get_context("spawn").Process(
|
|
952
|
+
target=process_target,
|
|
953
|
+
kwargs=process_args,
|
|
954
|
+
daemon=False, # Non-daemon to ensure it completes properly
|
|
955
|
+
)
|
|
956
|
+
process.start() # Set up file system event handler
|
|
957
|
+
|
|
958
|
+
class PyFileChangeHandler(FileSystemEventHandler):
|
|
959
|
+
def __init__(self) -> None:
|
|
960
|
+
self.last_modified_time = time.time()
|
|
961
|
+
self.debounce_seconds = 1.0 # Debounce to avoid multiple restarts
|
|
962
|
+
self.active_process = process
|
|
963
|
+
|
|
964
|
+
def on_modified(self, event: FileSystemEvent) -> None:
|
|
965
|
+
src_path = (
|
|
966
|
+
event.src_path
|
|
967
|
+
if isinstance(event.src_path, str)
|
|
968
|
+
else str(event.src_path)
|
|
969
|
+
)
|
|
970
|
+
|
|
971
|
+
# Ignore non-Python files and directories
|
|
972
|
+
if event.is_directory or not src_path.endswith(".py"):
|
|
973
|
+
return
|
|
974
|
+
|
|
975
|
+
# Debounce to avoid multiple restarts
|
|
976
|
+
current_time = time.time()
|
|
977
|
+
if current_time - self.last_modified_time < self.debounce_seconds:
|
|
978
|
+
return
|
|
979
|
+
self.last_modified_time = current_time
|
|
980
|
+
|
|
981
|
+
click.echo(f"Detected change in {src_path}")
|
|
982
|
+
click.echo("Restarting process...")
|
|
983
|
+
|
|
984
|
+
# Terminate the current process
|
|
985
|
+
if self.active_process and self.active_process.is_alive():
|
|
986
|
+
self.active_process.terminate()
|
|
987
|
+
self.active_process.join(timeout=5)
|
|
988
|
+
|
|
989
|
+
# If process doesn't terminate, kill it
|
|
990
|
+
if self.active_process.is_alive():
|
|
991
|
+
click.echo("Process did not terminate gracefully, killing it")
|
|
992
|
+
self.active_process.kill()
|
|
993
|
+
self.active_process.join()
|
|
994
|
+
|
|
995
|
+
# Create a new process
|
|
996
|
+
self.active_process = multiprocessing.get_context("spawn").Process(
|
|
997
|
+
target=process_target,
|
|
998
|
+
kwargs=process_args,
|
|
999
|
+
daemon=False,
|
|
1000
|
+
)
|
|
1001
|
+
self.active_process.start()
|
|
1002
|
+
|
|
1003
|
+
# Set up observer
|
|
1004
|
+
observer = Observer()
|
|
1005
|
+
observer.schedule(PyFileChangeHandler(), src_dir, recursive=True) # type: ignore[no-untyped-call]
|
|
1006
|
+
observer.start() # type: ignore[no-untyped-call]
|
|
1007
|
+
|
|
1008
|
+
click.echo(f"Watching for changes in {os.path.abspath(src_dir)}...")
|
|
1009
|
+
try:
|
|
1010
|
+
while True:
|
|
1011
|
+
time.sleep(1)
|
|
1012
|
+
except KeyboardInterrupt:
|
|
1013
|
+
observer.stop() # type: ignore[no-untyped-call]
|
|
1014
|
+
if process.is_alive():
|
|
1015
|
+
click.echo("Stopping process...")
|
|
1016
|
+
process.terminate()
|
|
1017
|
+
process.join(timeout=5)
|
|
1018
|
+
if process.is_alive():
|
|
1019
|
+
process.kill()
|
|
1020
|
+
process.join()
|
|
1021
|
+
click.echo("Reload mode stopped")
|
|
1022
|
+
observer.join()
|