jararaca 0.3.12a2__py3-none-any.whl → 0.3.12a4__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.
- jararaca/__init__.py +62 -4
- 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 +14 -6
- jararaca/tools/typescript/interface_parser.py +1 -1
- {jararaca-0.3.12a2.dist-info → jararaca-0.3.12a4.dist-info}/METADATA +1 -1
- {jararaca-0.3.12a2.dist-info → jararaca-0.3.12a4.dist-info}/RECORD +13 -13
- pyproject.toml +1 -1
- {jararaca-0.3.12a2.dist-info → jararaca-0.3.12a4.dist-info}/LICENSE +0 -0
- {jararaca-0.3.12a2.dist-info → jararaca-0.3.12a4.dist-info}/WHEEL +0 -0
- {jararaca-0.3.12a2.dist-info → jararaca-0.3.12a4.dist-info}/entry_points.txt +0 -0
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
|
|
@@ -242,6 +263,27 @@ if TYPE_CHECKING:
|
|
|
242
263
|
"provide_ws_manager",
|
|
243
264
|
"HttpRpcClientBuilder",
|
|
244
265
|
"HTTPXHttpRPCAsyncBackend",
|
|
266
|
+
# New request parameter decorators
|
|
267
|
+
"FormData",
|
|
268
|
+
"File",
|
|
269
|
+
# Configuration decorators
|
|
270
|
+
"Timeout",
|
|
271
|
+
"Retry",
|
|
272
|
+
"ContentType",
|
|
273
|
+
# Authentication classes
|
|
274
|
+
"BearerTokenAuth",
|
|
275
|
+
"BasicAuth",
|
|
276
|
+
"ApiKeyAuth",
|
|
277
|
+
# Middleware classes
|
|
278
|
+
"CacheMiddleware",
|
|
279
|
+
"AuthenticationMiddleware",
|
|
280
|
+
"ResponseMiddleware",
|
|
281
|
+
"RequestHook",
|
|
282
|
+
"ResponseHook",
|
|
283
|
+
# Configuration classes
|
|
284
|
+
"RetryConfig",
|
|
285
|
+
# Exception classes
|
|
286
|
+
"TimeoutException",
|
|
245
287
|
"use_app_context",
|
|
246
288
|
"use_app_transaction_context",
|
|
247
289
|
"use_app_tx_ctx_data",
|
|
@@ -440,6 +482,22 @@ _dynamic_imports: "dict[str, tuple[str, str, str | None]]" = {
|
|
|
440
482
|
"provide_ws_manager": (__SPEC_PARENT__, "presentation.websocket.context", None),
|
|
441
483
|
"HttpRpcClientBuilder": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
442
484
|
"HTTPXHttpRPCAsyncBackend": (__SPEC_PARENT__, "rpc.http.backends.httpx", None),
|
|
485
|
+
# New HTTP RPC classes
|
|
486
|
+
"FormData": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
487
|
+
"File": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
488
|
+
"Timeout": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
489
|
+
"Retry": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
490
|
+
"ContentType": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
491
|
+
"BearerTokenAuth": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
492
|
+
"BasicAuth": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
493
|
+
"ApiKeyAuth": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
494
|
+
"CacheMiddleware": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
495
|
+
"AuthenticationMiddleware": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
496
|
+
"ResponseMiddleware": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
497
|
+
"RequestHook": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
498
|
+
"ResponseHook": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
499
|
+
"RetryConfig": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
500
|
+
"TimeoutException": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
443
501
|
"use_app_context": (__SPEC_PARENT__, "microservice", None),
|
|
444
502
|
"use_app_transaction_context": (__SPEC_PARENT__, "microservice", None),
|
|
445
503
|
"use_app_tx_ctx_data": (__SPEC_PARENT__, "microservice", None),
|
jararaca/rpc/http/__init__.py
CHANGED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# HTTP RPC Module - Complete REST Client Implementation
|
|
2
|
+
"""
|
|
3
|
+
This module provides a complete REST client implementation with support for:
|
|
4
|
+
- HTTP method decorators (@Get, @Post, @Put, @Patch, @Delete)
|
|
5
|
+
- Request parameter decorators (@Query, @Header, @PathParam, @Body, @FormData, @File)
|
|
6
|
+
- Configuration decorators (@Timeout, @Retry, @ContentType)
|
|
7
|
+
- Authentication middleware (BearerTokenAuth, BasicAuth, ApiKeyAuth)
|
|
8
|
+
- Caching and response middleware
|
|
9
|
+
- Request/response hooks for customization
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .backends.httpx import HTTPXHttpRPCAsyncBackend
|
|
13
|
+
from .decorators import ( # HTTP Method decorators; Request parameter decorators; Configuration decorators; Client builder and core classes; Authentication classes; Middleware and hooks; Configuration classes; Data structures; Error handlers; Exceptions
|
|
14
|
+
ApiKeyAuth,
|
|
15
|
+
AuthenticationMiddleware,
|
|
16
|
+
BasicAuth,
|
|
17
|
+
BearerTokenAuth,
|
|
18
|
+
Body,
|
|
19
|
+
CacheMiddleware,
|
|
20
|
+
ContentType,
|
|
21
|
+
Delete,
|
|
22
|
+
File,
|
|
23
|
+
FormData,
|
|
24
|
+
Get,
|
|
25
|
+
GlobalHttpErrorHandler,
|
|
26
|
+
Header,
|
|
27
|
+
HttpMapping,
|
|
28
|
+
HttpRpcClientBuilder,
|
|
29
|
+
HttpRPCRequest,
|
|
30
|
+
HttpRPCResponse,
|
|
31
|
+
Patch,
|
|
32
|
+
PathParam,
|
|
33
|
+
Post,
|
|
34
|
+
Put,
|
|
35
|
+
Query,
|
|
36
|
+
RequestAttribute,
|
|
37
|
+
RequestHook,
|
|
38
|
+
ResponseHook,
|
|
39
|
+
ResponseMiddleware,
|
|
40
|
+
RestClient,
|
|
41
|
+
Retry,
|
|
42
|
+
RetryConfig,
|
|
43
|
+
RouteHttpErrorHandler,
|
|
44
|
+
RPCRequestNetworkError,
|
|
45
|
+
RPCUnhandleError,
|
|
46
|
+
Timeout,
|
|
47
|
+
TimeoutException,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
# HTTP Method decorators
|
|
52
|
+
"Get",
|
|
53
|
+
"Post",
|
|
54
|
+
"Put",
|
|
55
|
+
"Patch",
|
|
56
|
+
"Delete",
|
|
57
|
+
# Request parameter decorators
|
|
58
|
+
"Query",
|
|
59
|
+
"Header",
|
|
60
|
+
"PathParam",
|
|
61
|
+
"Body",
|
|
62
|
+
"FormData",
|
|
63
|
+
"File",
|
|
64
|
+
# Configuration decorators
|
|
65
|
+
"Timeout",
|
|
66
|
+
"Retry",
|
|
67
|
+
"ContentType",
|
|
68
|
+
# Client builder and core classes
|
|
69
|
+
"RestClient",
|
|
70
|
+
"HttpRpcClientBuilder",
|
|
71
|
+
"HttpMapping",
|
|
72
|
+
"RequestAttribute",
|
|
73
|
+
# Authentication classes
|
|
74
|
+
"BearerTokenAuth",
|
|
75
|
+
"BasicAuth",
|
|
76
|
+
"ApiKeyAuth",
|
|
77
|
+
"AuthenticationMiddleware",
|
|
78
|
+
# Middleware and hooks
|
|
79
|
+
"CacheMiddleware",
|
|
80
|
+
"ResponseMiddleware",
|
|
81
|
+
"RequestHook",
|
|
82
|
+
"ResponseHook",
|
|
83
|
+
# Configuration classes
|
|
84
|
+
"RetryConfig",
|
|
85
|
+
# Data structures
|
|
86
|
+
"HttpRPCRequest",
|
|
87
|
+
"HttpRPCResponse",
|
|
88
|
+
# Error handlers
|
|
89
|
+
"GlobalHttpErrorHandler",
|
|
90
|
+
"RouteHttpErrorHandler",
|
|
91
|
+
# Exceptions
|
|
92
|
+
"RPCRequestNetworkError",
|
|
93
|
+
"RPCUnhandleError",
|
|
94
|
+
"TimeoutException",
|
|
95
|
+
# Backend
|
|
96
|
+
"HTTPXHttpRPCAsyncBackend",
|
|
97
|
+
]
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import time
|
|
1
2
|
from urllib.parse import urljoin
|
|
2
3
|
|
|
3
4
|
import httpx
|
|
@@ -7,34 +8,63 @@ from jararaca.rpc.http.decorators import (
|
|
|
7
8
|
HttpRPCRequest,
|
|
8
9
|
HttpRPCResponse,
|
|
9
10
|
RPCRequestNetworkError,
|
|
11
|
+
TimeoutException,
|
|
10
12
|
)
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
class HTTPXHttpRPCAsyncBackend(HttpRPCAsyncBackend):
|
|
14
16
|
|
|
15
|
-
def __init__(self, prefix_url: str = ""):
|
|
17
|
+
def __init__(self, prefix_url: str = "", default_timeout: float = 30.0):
|
|
16
18
|
self.prefix_url = prefix_url
|
|
19
|
+
self.default_timeout = default_timeout
|
|
17
20
|
|
|
18
21
|
async def request(
|
|
19
22
|
self,
|
|
20
23
|
request: HttpRPCRequest,
|
|
21
24
|
) -> HttpRPCResponse:
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
start_time = time.time()
|
|
27
|
+
|
|
28
|
+
# Prepare timeout
|
|
29
|
+
timeout = (
|
|
30
|
+
request.timeout if request.timeout is not None else self.default_timeout
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Prepare request kwargs
|
|
34
|
+
request_kwargs = {
|
|
35
|
+
"method": request.method,
|
|
36
|
+
"url": urljoin(self.prefix_url, request.url),
|
|
37
|
+
"headers": request.headers,
|
|
38
|
+
"params": request.query_params,
|
|
39
|
+
"timeout": timeout,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Handle different content types
|
|
43
|
+
if request.form_data and request.files:
|
|
44
|
+
# Multipart form data with files
|
|
45
|
+
request_kwargs["data"] = request.form_data
|
|
46
|
+
request_kwargs["files"] = request.files
|
|
47
|
+
elif request.form_data:
|
|
48
|
+
# Form data only
|
|
49
|
+
request_kwargs["data"] = request.form_data
|
|
50
|
+
elif request.body:
|
|
51
|
+
# Raw body content
|
|
52
|
+
request_kwargs["content"] = request.body
|
|
24
53
|
|
|
54
|
+
async with httpx.AsyncClient() as client:
|
|
25
55
|
try:
|
|
26
|
-
response = await client.request(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
headers=request.headers,
|
|
30
|
-
params=request.query_params,
|
|
31
|
-
content=request.body,
|
|
32
|
-
)
|
|
56
|
+
response = await client.request(**request_kwargs) # type: ignore[arg-type]
|
|
57
|
+
|
|
58
|
+
elapsed_time = time.time() - start_time
|
|
33
59
|
|
|
34
60
|
return HttpRPCResponse(
|
|
35
61
|
status_code=response.status_code,
|
|
36
62
|
data=response.content,
|
|
63
|
+
headers=dict(response.headers),
|
|
64
|
+
elapsed_time=elapsed_time,
|
|
37
65
|
)
|
|
66
|
+
except httpx.TimeoutException as err:
|
|
67
|
+
raise TimeoutException(f"Request timed out: {err}") from err
|
|
38
68
|
except httpx.NetworkError as err:
|
|
39
69
|
raise RPCRequestNetworkError(
|
|
40
70
|
request=request, backend_request=err.request
|
jararaca/rpc/http/decorators.py
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import inspect
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
2
5
|
from dataclasses import dataclass
|
|
3
6
|
from typing import (
|
|
4
7
|
Any,
|
|
5
8
|
Awaitable,
|
|
6
9
|
Callable,
|
|
10
|
+
Dict,
|
|
7
11
|
Iterable,
|
|
8
12
|
Literal,
|
|
13
|
+
Optional,
|
|
9
14
|
Protocol,
|
|
10
15
|
Type,
|
|
11
16
|
TypeVar,
|
|
@@ -15,6 +20,11 @@ from typing import (
|
|
|
15
20
|
from pydantic import BaseModel
|
|
16
21
|
|
|
17
22
|
DECORATED_FUNC = TypeVar("DECORATED_FUNC", bound=Callable[..., Awaitable[Any]])
|
|
23
|
+
DECORATED_CLASS = TypeVar("DECORATED_CLASS", bound=Any)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TimeoutException(Exception):
|
|
27
|
+
"""Exception raised when a request times out"""
|
|
18
28
|
|
|
19
29
|
|
|
20
30
|
class HttpMapping:
|
|
@@ -95,7 +105,9 @@ class RequestAttribute:
|
|
|
95
105
|
return []
|
|
96
106
|
|
|
97
107
|
def __init__(
|
|
98
|
-
self,
|
|
108
|
+
self,
|
|
109
|
+
attribute_type: Literal["query", "header", "body", "param", "form", "file"],
|
|
110
|
+
name: str,
|
|
99
111
|
):
|
|
100
112
|
self.attribute_type = attribute_type
|
|
101
113
|
self.name = name
|
|
@@ -129,7 +141,105 @@ class PathParam(RequestAttribute):
|
|
|
129
141
|
super().__init__("param", name)
|
|
130
142
|
|
|
131
143
|
|
|
132
|
-
|
|
144
|
+
class FormData(RequestAttribute):
|
|
145
|
+
"""Decorator for form data parameters"""
|
|
146
|
+
|
|
147
|
+
def __init__(self, name: str):
|
|
148
|
+
super().__init__("form", name)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class File(RequestAttribute):
|
|
152
|
+
"""Decorator for file upload parameters"""
|
|
153
|
+
|
|
154
|
+
def __init__(self, name: str):
|
|
155
|
+
super().__init__("file", name)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class Timeout:
|
|
159
|
+
"""Decorator for setting request timeout"""
|
|
160
|
+
|
|
161
|
+
TIMEOUT_ATTR = "__request_timeout__"
|
|
162
|
+
|
|
163
|
+
def __init__(self, seconds: float):
|
|
164
|
+
self.seconds = seconds
|
|
165
|
+
|
|
166
|
+
def __call__(self, func: DECORATED_FUNC) -> DECORATED_FUNC:
|
|
167
|
+
setattr(func, self.TIMEOUT_ATTR, self)
|
|
168
|
+
return func
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def get(func: DECORATED_FUNC) -> Optional["Timeout"]:
|
|
172
|
+
return getattr(func, Timeout.TIMEOUT_ATTR, None)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class RetryConfig:
|
|
176
|
+
"""Configuration for retry behavior"""
|
|
177
|
+
|
|
178
|
+
def __init__(
|
|
179
|
+
self,
|
|
180
|
+
max_attempts: int = 3,
|
|
181
|
+
backoff_factor: float = 1.0,
|
|
182
|
+
retry_on_status_codes: Optional[list[int]] = None,
|
|
183
|
+
):
|
|
184
|
+
self.max_attempts = max_attempts
|
|
185
|
+
self.backoff_factor = backoff_factor
|
|
186
|
+
self.retry_on_status_codes = retry_on_status_codes or [500, 502, 503, 504]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class Retry:
|
|
190
|
+
"""Decorator for retry configuration"""
|
|
191
|
+
|
|
192
|
+
RETRY_ATTR = "__request_retry__"
|
|
193
|
+
|
|
194
|
+
def __init__(self, config: RetryConfig):
|
|
195
|
+
self.config = config
|
|
196
|
+
|
|
197
|
+
def __call__(self, func: DECORATED_FUNC) -> DECORATED_FUNC:
|
|
198
|
+
setattr(func, self.RETRY_ATTR, self)
|
|
199
|
+
return func
|
|
200
|
+
|
|
201
|
+
@staticmethod
|
|
202
|
+
def get(func: DECORATED_FUNC) -> Optional["Retry"]:
|
|
203
|
+
return getattr(func, Retry.RETRY_ATTR, None)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class ContentType:
|
|
207
|
+
"""Decorator for specifying content type"""
|
|
208
|
+
|
|
209
|
+
CONTENT_TYPE_ATTR = "__content_type__"
|
|
210
|
+
|
|
211
|
+
def __init__(self, content_type: str):
|
|
212
|
+
self.content_type = content_type
|
|
213
|
+
|
|
214
|
+
def __call__(self, func: DECORATED_FUNC) -> DECORATED_FUNC:
|
|
215
|
+
setattr(func, self.CONTENT_TYPE_ATTR, self)
|
|
216
|
+
return func
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
def get(func: DECORATED_FUNC) -> Optional["ContentType"]:
|
|
220
|
+
return getattr(func, ContentType.CONTENT_TYPE_ATTR, None)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class ResponseMiddleware(Protocol):
|
|
224
|
+
"""Protocol for response middleware"""
|
|
225
|
+
|
|
226
|
+
def on_response(
|
|
227
|
+
self, request: "HttpRPCRequest", response: "HttpRPCResponse"
|
|
228
|
+
) -> "HttpRPCResponse": ...
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class RequestHook(Protocol):
|
|
232
|
+
"""Protocol for request hooks"""
|
|
233
|
+
|
|
234
|
+
def before_request(self, request: "HttpRPCRequest") -> "HttpRPCRequest": ...
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class ResponseHook(Protocol):
|
|
238
|
+
"""Protocol for response hooks"""
|
|
239
|
+
|
|
240
|
+
def after_response(
|
|
241
|
+
self, request: "HttpRPCRequest", response: "HttpRPCResponse"
|
|
242
|
+
) -> "HttpRPCResponse": ...
|
|
133
243
|
|
|
134
244
|
|
|
135
245
|
class RestClient:
|
|
@@ -159,9 +269,10 @@ class RestClient:
|
|
|
159
269
|
|
|
160
270
|
@dataclass
|
|
161
271
|
class HttpRPCResponse:
|
|
162
|
-
|
|
163
272
|
status_code: int
|
|
164
273
|
data: bytes
|
|
274
|
+
headers: Optional[Dict[str, str]] = None
|
|
275
|
+
elapsed_time: Optional[float] = None
|
|
165
276
|
|
|
166
277
|
|
|
167
278
|
@dataclass
|
|
@@ -171,6 +282,9 @@ class HttpRPCRequest:
|
|
|
171
282
|
headers: list[tuple[str, str]]
|
|
172
283
|
query_params: dict[str, str]
|
|
173
284
|
body: bytes | None
|
|
285
|
+
timeout: Optional[float] = None
|
|
286
|
+
form_data: Optional[Dict[str, Any]] = None
|
|
287
|
+
files: Optional[Dict[str, Any]] = None
|
|
174
288
|
|
|
175
289
|
|
|
176
290
|
class RPCRequestNetworkError(Exception):
|
|
@@ -275,15 +389,130 @@ class RequestMiddleware(Protocol):
|
|
|
275
389
|
def on_request(self, request: HttpRPCRequest) -> HttpRPCRequest: ...
|
|
276
390
|
|
|
277
391
|
|
|
392
|
+
class AuthenticationMiddleware(RequestMiddleware):
|
|
393
|
+
"""Base class for authentication middleware"""
|
|
394
|
+
|
|
395
|
+
def on_request(self, request: HttpRPCRequest) -> HttpRPCRequest:
|
|
396
|
+
return self.add_auth(request)
|
|
397
|
+
|
|
398
|
+
def add_auth(self, request: HttpRPCRequest) -> HttpRPCRequest:
|
|
399
|
+
raise NotImplementedError
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class BearerTokenAuth(AuthenticationMiddleware):
|
|
403
|
+
"""Bearer token authentication middleware"""
|
|
404
|
+
|
|
405
|
+
def __init__(self, token: str):
|
|
406
|
+
self.token = token
|
|
407
|
+
|
|
408
|
+
def add_auth(self, request: HttpRPCRequest) -> HttpRPCRequest:
|
|
409
|
+
request.headers.append(("Authorization", f"Bearer {self.token}"))
|
|
410
|
+
return request
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class BasicAuth(AuthenticationMiddleware):
|
|
414
|
+
"""Basic authentication middleware"""
|
|
415
|
+
|
|
416
|
+
def __init__(self, username: str, password: str):
|
|
417
|
+
import base64
|
|
418
|
+
|
|
419
|
+
credentials = base64.b64encode(f"{username}:{password}".encode()).decode()
|
|
420
|
+
self.credentials = credentials
|
|
421
|
+
|
|
422
|
+
def add_auth(self, request: HttpRPCRequest) -> HttpRPCRequest:
|
|
423
|
+
request.headers.append(("Authorization", f"Basic {self.credentials}"))
|
|
424
|
+
return request
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
class ApiKeyAuth(AuthenticationMiddleware):
|
|
428
|
+
"""API key authentication middleware"""
|
|
429
|
+
|
|
430
|
+
def __init__(self, api_key: str, header_name: str = "X-API-Key"):
|
|
431
|
+
self.api_key = api_key
|
|
432
|
+
self.header_name = header_name
|
|
433
|
+
|
|
434
|
+
def add_auth(self, request: HttpRPCRequest) -> HttpRPCRequest:
|
|
435
|
+
request.headers.append((self.header_name, self.api_key))
|
|
436
|
+
return request
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
class CacheMiddleware(RequestMiddleware):
|
|
440
|
+
"""Simple in-memory cache middleware"""
|
|
441
|
+
|
|
442
|
+
def __init__(self, ttl_seconds: int = 300):
|
|
443
|
+
self.cache: Dict[str, tuple[Any, float]] = {}
|
|
444
|
+
self.ttl_seconds = ttl_seconds
|
|
445
|
+
|
|
446
|
+
def _cache_key(self, request: HttpRPCRequest) -> str:
|
|
447
|
+
"""Generate cache key from request"""
|
|
448
|
+
key_data = {
|
|
449
|
+
"method": request.method,
|
|
450
|
+
"url": request.url,
|
|
451
|
+
"query_params": request.query_params,
|
|
452
|
+
"headers": sorted(request.headers),
|
|
453
|
+
}
|
|
454
|
+
return str(hash(json.dumps(key_data, sort_keys=True)))
|
|
455
|
+
|
|
456
|
+
def on_request(self, request: HttpRPCRequest) -> HttpRPCRequest:
|
|
457
|
+
# Only cache GET requests
|
|
458
|
+
if request.method == "GET":
|
|
459
|
+
cache_key = self._cache_key(request)
|
|
460
|
+
if cache_key in self.cache:
|
|
461
|
+
cached_response, timestamp = self.cache[cache_key]
|
|
462
|
+
if time.time() - timestamp < self.ttl_seconds:
|
|
463
|
+
# Return cached response (this needs to be handled in the client builder)
|
|
464
|
+
setattr(request, "_cached_response", cached_response)
|
|
465
|
+
return request
|
|
466
|
+
|
|
467
|
+
|
|
278
468
|
class HttpRpcClientBuilder:
|
|
279
469
|
|
|
280
470
|
def __init__(
|
|
281
471
|
self,
|
|
282
472
|
backend: HttpRPCAsyncBackend,
|
|
283
473
|
middlewares: list[RequestMiddleware] = [],
|
|
474
|
+
response_middlewares: list[ResponseMiddleware] = [],
|
|
475
|
+
request_hooks: list[RequestHook] = [],
|
|
476
|
+
response_hooks: list[ResponseHook] = [],
|
|
284
477
|
):
|
|
285
478
|
self._backend = backend
|
|
286
479
|
self._middlewares = middlewares
|
|
480
|
+
self._response_middlewares = response_middlewares
|
|
481
|
+
self._request_hooks = request_hooks
|
|
482
|
+
self._response_hooks = response_hooks
|
|
483
|
+
|
|
484
|
+
async def _execute_with_retry(
|
|
485
|
+
self, request: HttpRPCRequest, retry_config: Optional[RetryConfig]
|
|
486
|
+
) -> HttpRPCResponse:
|
|
487
|
+
"""Execute request with retry logic"""
|
|
488
|
+
if not retry_config:
|
|
489
|
+
return await self._backend.request(request)
|
|
490
|
+
|
|
491
|
+
last_exception = None
|
|
492
|
+
for attempt in range(retry_config.max_attempts):
|
|
493
|
+
try:
|
|
494
|
+
response = await self._backend.request(request)
|
|
495
|
+
|
|
496
|
+
# Check if we should retry based on status code
|
|
497
|
+
if response.status_code in retry_config.retry_on_status_codes:
|
|
498
|
+
if attempt < retry_config.max_attempts - 1:
|
|
499
|
+
wait_time = retry_config.backoff_factor * (2**attempt)
|
|
500
|
+
await asyncio.sleep(wait_time)
|
|
501
|
+
continue
|
|
502
|
+
|
|
503
|
+
return response
|
|
504
|
+
|
|
505
|
+
except Exception as e:
|
|
506
|
+
last_exception = e
|
|
507
|
+
if attempt < retry_config.max_attempts - 1:
|
|
508
|
+
wait_time = retry_config.backoff_factor * (2**attempt)
|
|
509
|
+
await asyncio.sleep(wait_time)
|
|
510
|
+
continue
|
|
511
|
+
else:
|
|
512
|
+
raise
|
|
513
|
+
|
|
514
|
+
# This should never be reached, but just in case
|
|
515
|
+
raise last_exception or Exception("Retry failed")
|
|
287
516
|
|
|
288
517
|
def build(self, cls: type[T]) -> T:
|
|
289
518
|
rest_client = RestClient.get(cls)
|
|
@@ -314,10 +543,17 @@ class HttpRpcClientBuilder:
|
|
|
314
543
|
headers: list[tuple[str, str]] = []
|
|
315
544
|
query_params = {}
|
|
316
545
|
body: Any = None
|
|
546
|
+
form_data: Dict[str, Any] = {}
|
|
547
|
+
files: Dict[str, Any] = {}
|
|
317
548
|
compiled_path = (
|
|
318
549
|
rest_client.base_path.rstrip("/") + "/" + mapping.path.lstrip("/")
|
|
319
550
|
)
|
|
320
551
|
|
|
552
|
+
# Get decorators for this method
|
|
553
|
+
timeout_config = Timeout.get(method_call)
|
|
554
|
+
retry_config = Retry.get(method_call)
|
|
555
|
+
content_type_config = ContentType.get(method_call)
|
|
556
|
+
|
|
321
557
|
for attr in request_attributes:
|
|
322
558
|
if attr.attribute_type == "header":
|
|
323
559
|
headers.append((attr.name, compiled_kwargs[attr.name]))
|
|
@@ -329,17 +565,33 @@ class HttpRpcClientBuilder:
|
|
|
329
565
|
compiled_path = compiled_path.replace(
|
|
330
566
|
f":{attr.name}", str(compiled_kwargs[attr.name])
|
|
331
567
|
)
|
|
568
|
+
elif attr.attribute_type == "form":
|
|
569
|
+
form_data[attr.name] = compiled_kwargs[attr.name]
|
|
570
|
+
elif attr.attribute_type == "file":
|
|
571
|
+
files[attr.name] = compiled_kwargs[attr.name]
|
|
332
572
|
|
|
333
573
|
body_content: bytes | None = None
|
|
334
574
|
|
|
575
|
+
# Handle different content types
|
|
335
576
|
if body is not None:
|
|
336
577
|
if isinstance(body, BaseModel):
|
|
337
578
|
body_content = body.model_dump_json().encode()
|
|
338
|
-
|
|
579
|
+
if not content_type_config:
|
|
580
|
+
headers.append(("Content-Type", "application/json"))
|
|
339
581
|
elif isinstance(body, bytes):
|
|
340
582
|
body_content = body
|
|
583
|
+
elif isinstance(body, str):
|
|
584
|
+
body_content = body.encode()
|
|
585
|
+
elif isinstance(body, dict):
|
|
586
|
+
body_content = json.dumps(body).encode()
|
|
587
|
+
if not content_type_config:
|
|
588
|
+
headers.append(("Content-Type", "application/json"))
|
|
341
589
|
else:
|
|
342
|
-
raise ValueError("Invalid body type")
|
|
590
|
+
raise ValueError(f"Invalid body type: {type(body)}")
|
|
591
|
+
|
|
592
|
+
# Apply custom content type if specified
|
|
593
|
+
if content_type_config:
|
|
594
|
+
headers.append(("Content-Type", content_type_config.content_type))
|
|
343
595
|
|
|
344
596
|
request = HttpRPCRequest(
|
|
345
597
|
url=compiled_path,
|
|
@@ -347,12 +599,41 @@ class HttpRpcClientBuilder:
|
|
|
347
599
|
headers=headers,
|
|
348
600
|
query_params=query_params,
|
|
349
601
|
body=body_content,
|
|
602
|
+
timeout=timeout_config.seconds if timeout_config else None,
|
|
603
|
+
form_data=form_data if form_data else None,
|
|
604
|
+
files=files if files else None,
|
|
350
605
|
)
|
|
351
606
|
|
|
607
|
+
# Apply request hooks
|
|
608
|
+
for hook in self._request_hooks:
|
|
609
|
+
request = hook.before_request(request)
|
|
610
|
+
|
|
352
611
|
for middleware in self._middlewares:
|
|
353
612
|
request = middleware.on_request(request)
|
|
354
613
|
|
|
355
|
-
|
|
614
|
+
# Check for cached response
|
|
615
|
+
if hasattr(request, "_cached_response"):
|
|
616
|
+
response = getattr(request, "_cached_response")
|
|
617
|
+
else:
|
|
618
|
+
# Execute request with retry if configured
|
|
619
|
+
response = await self._execute_with_retry(
|
|
620
|
+
request, retry_config.config if retry_config else None
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
# Apply response middleware
|
|
624
|
+
for response_middleware in self._response_middlewares:
|
|
625
|
+
response = response_middleware.on_response(request, response)
|
|
626
|
+
|
|
627
|
+
# Apply response hooks
|
|
628
|
+
for response_hook in self._response_hooks:
|
|
629
|
+
response = response_hook.after_response(request, response)
|
|
630
|
+
|
|
631
|
+
# Cache response if using cache middleware and it's a GET request
|
|
632
|
+
if request.method == "GET":
|
|
633
|
+
for middleware in self._middlewares:
|
|
634
|
+
if isinstance(middleware, CacheMiddleware):
|
|
635
|
+
cache_key = middleware._cache_key(request)
|
|
636
|
+
middleware.cache[cache_key] = (response, time.time())
|
|
356
637
|
|
|
357
638
|
return_type = inspect.signature(method_call).return_annotation
|
|
358
639
|
|
|
@@ -407,13 +688,28 @@ __all__ = [
|
|
|
407
688
|
"Header",
|
|
408
689
|
"Body",
|
|
409
690
|
"PathParam",
|
|
691
|
+
"FormData",
|
|
692
|
+
"File",
|
|
693
|
+
"Timeout",
|
|
694
|
+
"RetryConfig",
|
|
695
|
+
"Retry",
|
|
696
|
+
"ContentType",
|
|
410
697
|
"RestClient",
|
|
411
698
|
"HttpRPCAsyncBackend",
|
|
412
699
|
"HttpRPCRequest",
|
|
413
700
|
"HttpRPCResponse",
|
|
414
701
|
"RPCRequestNetworkError",
|
|
702
|
+
"RPCUnhandleError",
|
|
415
703
|
"HttpRpcClientBuilder",
|
|
416
704
|
"RequestMiddleware",
|
|
705
|
+
"ResponseMiddleware",
|
|
706
|
+
"RequestHook",
|
|
707
|
+
"ResponseHook",
|
|
708
|
+
"AuthenticationMiddleware",
|
|
709
|
+
"BearerTokenAuth",
|
|
710
|
+
"BasicAuth",
|
|
711
|
+
"ApiKeyAuth",
|
|
712
|
+
"CacheMiddleware",
|
|
417
713
|
"TracedRequestMiddleware",
|
|
418
714
|
"GlobalHttpErrorHandler",
|
|
419
715
|
"RouteHttpErrorHandler",
|
|
@@ -5,7 +5,6 @@ import signal
|
|
|
5
5
|
import time
|
|
6
6
|
from abc import ABC, abstractmethod
|
|
7
7
|
from datetime import UTC, datetime
|
|
8
|
-
from types import FrameType
|
|
9
8
|
from typing import Any
|
|
10
9
|
from urllib.parse import parse_qs
|
|
11
10
|
|
|
@@ -249,13 +248,16 @@ class BeatWorker:
|
|
|
249
248
|
|
|
250
249
|
def run(self) -> None:
|
|
251
250
|
|
|
252
|
-
def
|
|
253
|
-
logger.info("
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
signal.signal(signal.SIGINT, on_signal_received)
|
|
251
|
+
def on_shutdown(loop: asyncio.AbstractEventLoop) -> None:
|
|
252
|
+
logger.info("Shutting down - signal received")
|
|
253
|
+
# Schedule the shutdown to run in the event loop
|
|
254
|
+
asyncio.create_task(self._graceful_shutdown())
|
|
257
255
|
|
|
258
256
|
with asyncio.Runner(loop_factory=uvloop.new_event_loop) as runner:
|
|
257
|
+
loop = runner.get_loop()
|
|
258
|
+
loop.add_signal_handler(signal.SIGINT, on_shutdown, loop)
|
|
259
|
+
# Add graceful shutdown handler for SIGTERM as well
|
|
260
|
+
loop.add_signal_handler(signal.SIGTERM, on_shutdown, loop)
|
|
259
261
|
runner.run(self.start_scheduler())
|
|
260
262
|
|
|
261
263
|
async def start_scheduler(self) -> None:
|
|
@@ -340,3 +342,9 @@ class BeatWorker:
|
|
|
340
342
|
|
|
341
343
|
await self.backend.dispose()
|
|
342
344
|
await self.broker.dispose()
|
|
345
|
+
|
|
346
|
+
async def _graceful_shutdown(self) -> None:
|
|
347
|
+
"""Handles graceful shutdown process"""
|
|
348
|
+
logger.info("Initiating graceful shutdown sequence")
|
|
349
|
+
self.shutdown_event.set()
|
|
350
|
+
logger.info("Graceful shutdown completed")
|
|
@@ -406,7 +406,7 @@ export type ResponseType =
|
|
|
406
406
|
export interface HttpBackendRequest {
|
|
407
407
|
method: string;
|
|
408
408
|
path: string;
|
|
409
|
-
pathParams: { [key: string]:
|
|
409
|
+
pathParams: { [key: string]: any };
|
|
410
410
|
headers: { [key: string]: string };
|
|
411
411
|
query: { [key: string]: unknown };
|
|
412
412
|
body: unknown;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
2
2
|
README.md,sha256=2qMM__t_MoLKZr4IY9tXjo-Jn6LKjuHMb1qbyXpgL08,3401
|
|
3
|
-
pyproject.toml,sha256=
|
|
4
|
-
jararaca/__init__.py,sha256=
|
|
3
|
+
pyproject.toml,sha256=52W7n20RKPz82FFphOmYyMWBVaYSyaAsIDUYwh9YACQ,2040
|
|
4
|
+
jararaca/__init__.py,sha256=Jszgq9WCKnriBezBtvhj35aSsJWnTUXm9-UfThkDT3s,20701
|
|
5
5
|
jararaca/__main__.py,sha256=-O3vsB5lHdqNFjUtoELDF81IYFtR-DSiiFMzRaiSsv4,67
|
|
6
6
|
jararaca/broker_backend/__init__.py,sha256=GzEIuHR1xzgCJD4FE3harNjoaYzxHMHoEL0_clUaC-k,3528
|
|
7
7
|
jararaca/broker_backend/mapper.py,sha256=vTsi7sWpNvlga1PWPFg0rCJ5joJ0cdzykkIc2Tuvenc,696
|
|
@@ -53,25 +53,25 @@ jararaca/reflect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
|
|
|
53
53
|
jararaca/reflect/controller_inspect.py,sha256=UtV4pRIOqCoK4ogBTXQE0dyopEQ5LDFhwm-1iJvrkJc,2326
|
|
54
54
|
jararaca/reflect/metadata.py,sha256=oTi0zIjCYkeBhs12PNTLc8GmzR6qWHdl3drlmamXLJo,1897
|
|
55
55
|
jararaca/rpc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
56
|
-
jararaca/rpc/http/__init__.py,sha256=
|
|
57
|
-
jararaca/rpc/http/backends/__init__.py,sha256=
|
|
58
|
-
jararaca/rpc/http/backends/httpx.py,sha256=
|
|
56
|
+
jararaca/rpc/http/__init__.py,sha256=Xp7-d-cVj7EK1JloSUbSnGBfER5YwCfp7LCU6wCAf1c,2396
|
|
57
|
+
jararaca/rpc/http/backends/__init__.py,sha256=Q1tIj1PTjB4__qTZndGMu4IjP5lbayLbQZJ4fZXcnAk,166
|
|
58
|
+
jararaca/rpc/http/backends/httpx.py,sha256=nNHXBortLt0tWW1qTXMPtSuO1mkpQcNh5eplP_8zYEM,2246
|
|
59
59
|
jararaca/rpc/http/backends/otel.py,sha256=Uc6CjHSCZ5hvnK1fNFv3ota5xzUFnvIl1JOpG380siA,807
|
|
60
|
-
jararaca/rpc/http/decorators.py,sha256=
|
|
60
|
+
jararaca/rpc/http/decorators.py,sha256=0BCcmP_jolIAB73VeXm3TKyVr3AWbzkXzUVMGqYtqSQ,22145
|
|
61
61
|
jararaca/rpc/http/httpx.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
62
62
|
jararaca/scheduler/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
63
|
-
jararaca/scheduler/beat_worker.py,sha256=
|
|
63
|
+
jararaca/scheduler/beat_worker.py,sha256=i_NyovjFhhLYuUivf2mwpVC27oQ9SGTFOHZ7Ec4jv6I,12076
|
|
64
64
|
jararaca/scheduler/decorators.py,sha256=iyWFvPLCRh9c0YReQRemI2mLuaUv7r929So-xuKIWUs,4605
|
|
65
65
|
jararaca/scheduler/types.py,sha256=4HEQOmVIDp-BYLSzqmqSFIio1bd51WFmgFPIzPpVu04,135
|
|
66
66
|
jararaca/tools/app_config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
67
67
|
jararaca/tools/app_config/decorators.py,sha256=-ckkMZ1dswOmECdo1rFrZ15UAku--txaNXMp8fd1Ndk,941
|
|
68
68
|
jararaca/tools/app_config/interceptor.py,sha256=HV8h4AxqUc_ACs5do4BSVlyxlRXzx7HqJtoVO9tfRnQ,2611
|
|
69
|
-
jararaca/tools/typescript/interface_parser.py,sha256=
|
|
69
|
+
jararaca/tools/typescript/interface_parser.py,sha256=foZ_A_lkyacmAq9TU6hUFv5ZnZcPZbbxcjtzFUu1A_o,31479
|
|
70
70
|
jararaca/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
71
71
|
jararaca/utils/rabbitmq_utils.py,sha256=ytdAFUyv-OBkaVnxezuJaJoLrmN7giZgtKeet_IsMBs,10918
|
|
72
72
|
jararaca/utils/retry.py,sha256=DzPX_fXUvTqej6BQ8Mt2dvLo9nNlTBm7Kx2pFZ26P2Q,4668
|
|
73
|
-
jararaca-0.3.
|
|
74
|
-
jararaca-0.3.
|
|
75
|
-
jararaca-0.3.
|
|
76
|
-
jararaca-0.3.
|
|
77
|
-
jararaca-0.3.
|
|
73
|
+
jararaca-0.3.12a4.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
74
|
+
jararaca-0.3.12a4.dist-info/METADATA,sha256=ZhivHS3NMe_L-0vt0uS_jilL4cSNkPGUtGLhX4ygD7U,4995
|
|
75
|
+
jararaca-0.3.12a4.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
76
|
+
jararaca-0.3.12a4.dist-info/entry_points.txt,sha256=WIh3aIvz8LwUJZIDfs4EeH3VoFyCGEk7cWJurW38q0I,45
|
|
77
|
+
jararaca-0.3.12a4.dist-info/RECORD,,
|
pyproject.toml
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|