axios-python 0.1.0__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.
- axios_python/__init__.py +148 -0
- axios_python/cancel/__init__.py +1 -0
- axios_python/cancel/exceptions.py +9 -0
- axios_python/cancel/token.py +74 -0
- axios_python/client.py +349 -0
- axios_python/config.py +47 -0
- axios_python/defaults.py +26 -0
- axios_python/exceptions.py +47 -0
- axios_python/interceptors/__init__.py +1 -0
- axios_python/interceptors/chain.py +118 -0
- axios_python/interceptors/manager.py +29 -0
- axios_python/middleware/__init__.py +1 -0
- axios_python/middleware/manager.py +54 -0
- axios_python/middleware/pipeline.py +68 -0
- axios_python/plugins/__init__.py +1 -0
- axios_python/plugins/auth.py +49 -0
- axios_python/plugins/base.py +27 -0
- axios_python/plugins/cache.py +75 -0
- axios_python/plugins/logger.py +57 -0
- axios_python/request.py +23 -0
- axios_python/response.py +137 -0
- axios_python/retry/__init__.py +1 -0
- axios_python/retry/engine.py +105 -0
- axios_python/retry/strategy.py +116 -0
- axios_python/transport/__init__.py +1 -0
- axios_python/transport/base.py +50 -0
- axios_python/transport/httpx_adapter.py +131 -0
- axios_python/utils/__init__.py +1 -0
- axios_python/utils/async_utils.py +56 -0
- axios_python/utils/merge.py +33 -0
- axios_python-0.1.0.dist-info/METADATA +348 -0
- axios_python-0.1.0.dist-info/RECORD +33 -0
- axios_python-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
from axios_python.exceptions import InterceptorError
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"InterceptorChain",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
HandlerFn = Callable[[Any], Any]
|
|
12
|
+
ErrorHandlerFn = Callable[[Exception], Any]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InterceptorChain:
|
|
16
|
+
"""An ordered sequence of interceptor handler pairs.
|
|
17
|
+
|
|
18
|
+
Each entry is a ``(fulfilled, rejected)`` tuple. During execution the
|
|
19
|
+
chain pipes the value through each *fulfilled* handler in order. If any
|
|
20
|
+
handler raises, the corresponding *rejected* handler (if provided) is
|
|
21
|
+
called, otherwise the exception propagates.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
self._handlers: list[tuple[int, HandlerFn, ErrorHandlerFn | None]] = []
|
|
26
|
+
self._next_id: int = 0
|
|
27
|
+
|
|
28
|
+
def use(
|
|
29
|
+
self,
|
|
30
|
+
fulfilled: HandlerFn,
|
|
31
|
+
rejected: ErrorHandlerFn | None = None,
|
|
32
|
+
) -> int:
|
|
33
|
+
"""Register a new interceptor handler pair.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
fulfilled: Called with the current value when the chain succeeds.
|
|
37
|
+
rejected: Called with the exception when a prior handler raises.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
An integer id that can be passed to :meth:`eject`.
|
|
41
|
+
"""
|
|
42
|
+
handler_id = self._next_id
|
|
43
|
+
self._next_id += 1
|
|
44
|
+
self._handlers.append((handler_id, fulfilled, rejected))
|
|
45
|
+
return handler_id
|
|
46
|
+
|
|
47
|
+
def eject(self, handler_id: int) -> None:
|
|
48
|
+
"""Remove a previously registered interceptor by its id.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
handler_id: The id returned by :meth:`use`.
|
|
52
|
+
"""
|
|
53
|
+
self._handlers = [
|
|
54
|
+
h for h in self._handlers if h[0] != handler_id
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
def run(self, initial: Any) -> Any:
|
|
58
|
+
"""Execute the chain synchronously.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
initial: The starting value passed to the first handler.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
The value produced by the final handler.
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
InterceptorError: If a handler raises and no rejected handler
|
|
68
|
+
catches the error.
|
|
69
|
+
"""
|
|
70
|
+
value = initial
|
|
71
|
+
for _, fulfilled, rejected in self._handlers:
|
|
72
|
+
try:
|
|
73
|
+
value = fulfilled(value)
|
|
74
|
+
except Exception as exc:
|
|
75
|
+
if rejected is not None:
|
|
76
|
+
value = rejected(exc)
|
|
77
|
+
else:
|
|
78
|
+
raise InterceptorError(str(exc)) from exc
|
|
79
|
+
return value
|
|
80
|
+
|
|
81
|
+
async def run_async(self, initial: Any) -> Any:
|
|
82
|
+
"""Execute the chain asynchronously.
|
|
83
|
+
|
|
84
|
+
Await handlers that return coroutines.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
initial: The starting value passed to the first handler.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
The value produced by the final handler.
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
InterceptorError: If a handler raises and no rejected handler
|
|
94
|
+
catches the error.
|
|
95
|
+
"""
|
|
96
|
+
import inspect
|
|
97
|
+
|
|
98
|
+
value = initial
|
|
99
|
+
for _, fulfilled, rejected in self._handlers:
|
|
100
|
+
try:
|
|
101
|
+
result = fulfilled(value)
|
|
102
|
+
if inspect.isawaitable(result):
|
|
103
|
+
value = await result
|
|
104
|
+
else:
|
|
105
|
+
value = result
|
|
106
|
+
except Exception as exc:
|
|
107
|
+
if rejected is not None:
|
|
108
|
+
result = rejected(exc)
|
|
109
|
+
if inspect.isawaitable(result):
|
|
110
|
+
value = await result
|
|
111
|
+
else:
|
|
112
|
+
value = result
|
|
113
|
+
else:
|
|
114
|
+
raise InterceptorError(str(exc)) from exc
|
|
115
|
+
return value
|
|
116
|
+
|
|
117
|
+
def __len__(self) -> int:
|
|
118
|
+
return len(self._handlers)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
from axios_python.interceptors.chain import InterceptorChain
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"InterceptorManager",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
HandlerFn = Callable[[Any], Any]
|
|
12
|
+
ErrorHandlerFn = Callable[[Exception], Any]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InterceptorManager:
|
|
16
|
+
"""Manages request and response interceptor chains.
|
|
17
|
+
|
|
18
|
+
Access via ``client.interceptors.request`` and
|
|
19
|
+
``client.interceptors.response``.
|
|
20
|
+
|
|
21
|
+
Example::
|
|
22
|
+
|
|
23
|
+
api.interceptors.request.use(lambda cfg: cfg)
|
|
24
|
+
api.interceptors.response.use(lambda res: res)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
self.request: InterceptorChain = InterceptorChain()
|
|
29
|
+
self.response: InterceptorChain = InterceptorChain()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Awaitable, Callable
|
|
4
|
+
|
|
5
|
+
from axios_python.middleware.pipeline import Pipeline
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"MiddlewareManager",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
MiddlewareFn = Callable[[dict[str, Any], Callable[..., Awaitable[Any]]], Awaitable[Any]]
|
|
12
|
+
FinalHandler = Callable[[dict[str, Any]], Awaitable[Any]]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MiddlewareManager:
|
|
16
|
+
"""Manages the middleware pipeline for a client instance.
|
|
17
|
+
|
|
18
|
+
Example::
|
|
19
|
+
|
|
20
|
+
async def timing(ctx, next):
|
|
21
|
+
import time
|
|
22
|
+
start = time.monotonic()
|
|
23
|
+
result = await next(ctx)
|
|
24
|
+
ctx["elapsed"] = time.monotonic() - start
|
|
25
|
+
return result
|
|
26
|
+
|
|
27
|
+
api.use(timing)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self) -> None:
|
|
31
|
+
self._pipeline: Pipeline = Pipeline()
|
|
32
|
+
|
|
33
|
+
def use(self, fn: MiddlewareFn) -> None:
|
|
34
|
+
"""Register a middleware function.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
fn: An async callable with signature ``(ctx, next) -> Any``.
|
|
38
|
+
"""
|
|
39
|
+
self._pipeline.use(fn)
|
|
40
|
+
|
|
41
|
+
async def execute(self, ctx: dict[str, Any], final: FinalHandler) -> Any:
|
|
42
|
+
"""Execute the full middleware stack, terminating with *final*.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
ctx: A mutable context dict.
|
|
46
|
+
final: The terminal handler.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
The result produced by the pipeline.
|
|
50
|
+
"""
|
|
51
|
+
return await self._pipeline.execute(ctx, final)
|
|
52
|
+
|
|
53
|
+
def __len__(self) -> int:
|
|
54
|
+
return len(self._pipeline)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Awaitable, Callable
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"Pipeline",
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
MiddlewareFn = Callable[[dict[str, Any], Callable[..., Awaitable[Any]]], Awaitable[Any]]
|
|
10
|
+
FinalHandler = Callable[[dict[str, Any]], Awaitable[Any]]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Pipeline:
|
|
14
|
+
"""Express-style middleware pipeline.
|
|
15
|
+
|
|
16
|
+
Middleware functions have the signature::
|
|
17
|
+
|
|
18
|
+
async def my_middleware(ctx: dict, next: Callable) -> Any:
|
|
19
|
+
# pre-processing
|
|
20
|
+
result = await next(ctx)
|
|
21
|
+
# post-processing
|
|
22
|
+
return result
|
|
23
|
+
|
|
24
|
+
The pipeline composes all registered middleware into a single callable
|
|
25
|
+
that terminates in a *final handler*.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
self._stack: list[MiddlewareFn] = []
|
|
30
|
+
|
|
31
|
+
def use(self, fn: MiddlewareFn) -> None:
|
|
32
|
+
"""Add a middleware function to the pipeline.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
fn: An async callable with signature ``(ctx, next) -> Any``.
|
|
36
|
+
"""
|
|
37
|
+
self._stack.append(fn)
|
|
38
|
+
|
|
39
|
+
async def execute(self, ctx: dict[str, Any], final: FinalHandler) -> Any:
|
|
40
|
+
"""Run the pipeline and terminate with *final*.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
ctx: A mutable context dict shared across all middleware.
|
|
44
|
+
final: The terminal handler invoked after all middleware.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The result produced by the pipeline.
|
|
48
|
+
"""
|
|
49
|
+
index = -1
|
|
50
|
+
|
|
51
|
+
async def dispatch(i: int, c: dict[str, Any]) -> Any:
|
|
52
|
+
nonlocal index
|
|
53
|
+
if i <= index:
|
|
54
|
+
raise RuntimeError("next() called multiple times")
|
|
55
|
+
index = i
|
|
56
|
+
if i < len(self._stack):
|
|
57
|
+
mw = self._stack[i]
|
|
58
|
+
|
|
59
|
+
async def next_fn(updated_ctx: dict[str, Any] | None = None) -> Any:
|
|
60
|
+
return await dispatch(i + 1, updated_ctx if updated_ctx is not None else c)
|
|
61
|
+
|
|
62
|
+
return await mw(c, next_fn)
|
|
63
|
+
return await final(c)
|
|
64
|
+
|
|
65
|
+
return await dispatch(0, ctx)
|
|
66
|
+
|
|
67
|
+
def __len__(self) -> int:
|
|
68
|
+
return len(self._stack)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from axios_python.client import AxiosPython
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"AuthPlugin",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AuthPlugin:
|
|
14
|
+
"""Plugin that injects an Authorization header into every request.
|
|
15
|
+
|
|
16
|
+
Supports static tokens and dynamic token providers.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
token: A static bearer token string.
|
|
20
|
+
token_provider: A callable that returns a token string at call
|
|
21
|
+
time (takes precedence over *token* if both are provided).
|
|
22
|
+
scheme: The authorization scheme (default: ``"Bearer"``).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
token: str | None = None,
|
|
28
|
+
token_provider: Callable[[], str] | None = None,
|
|
29
|
+
scheme: str = "Bearer",
|
|
30
|
+
) -> None:
|
|
31
|
+
self._token = token
|
|
32
|
+
self._token_provider = token_provider
|
|
33
|
+
self._scheme = scheme
|
|
34
|
+
|
|
35
|
+
def install(self, client: AxiosPython) -> None:
|
|
36
|
+
"""Register a request interceptor that sets the Authorization header.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
client: The AxiosPython client to extend.
|
|
40
|
+
"""
|
|
41
|
+
client.interceptors.request.use(self._inject_auth)
|
|
42
|
+
|
|
43
|
+
def _inject_auth(self, config: dict[str, Any]) -> dict[str, Any]:
|
|
44
|
+
token = self._token_provider() if self._token_provider else self._token
|
|
45
|
+
if token:
|
|
46
|
+
headers = dict(config.get("headers", {}))
|
|
47
|
+
headers["Authorization"] = f"{self._scheme} {token}"
|
|
48
|
+
config["headers"] = headers
|
|
49
|
+
return config
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from axios_python.client import AxiosPython
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Plugin",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@runtime_checkable
|
|
14
|
+
class Plugin(Protocol):
|
|
15
|
+
"""Protocol that all axios_python plugins must implement.
|
|
16
|
+
|
|
17
|
+
A plugin receives the client instance during installation and may
|
|
18
|
+
register interceptors, middleware, or perform other setup.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def install(self, client: AxiosPython) -> None:
|
|
22
|
+
"""Install this plugin onto a client instance.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
client: The AxiosPython client to extend.
|
|
26
|
+
"""
|
|
27
|
+
...
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Awaitable, Callable
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from axios_python.client import AxiosPython
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"CachePlugin",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CachePlugin:
|
|
15
|
+
"""In-memory TTL cache plugin for GET requests.
|
|
16
|
+
|
|
17
|
+
Caches responses in memory keyed by full URL. Only GET requests
|
|
18
|
+
are cached.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
ttl: Time-to-live in seconds for cached entries.
|
|
22
|
+
max_size: Maximum number of cached entries.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, ttl: float = 60.0, max_size: int = 128) -> None:
|
|
26
|
+
self._ttl = ttl
|
|
27
|
+
self._max_size = max_size
|
|
28
|
+
self._store: dict[str, tuple[float, Any]] = {}
|
|
29
|
+
|
|
30
|
+
def install(self, client: AxiosPython) -> None:
|
|
31
|
+
"""Register a cache middleware on the client.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
client: The AxiosPython client to extend.
|
|
35
|
+
"""
|
|
36
|
+
plugin = self
|
|
37
|
+
|
|
38
|
+
async def cache_middleware(
|
|
39
|
+
ctx: dict[str, Any],
|
|
40
|
+
next_fn: Callable[..., Awaitable[Any]],
|
|
41
|
+
) -> Any:
|
|
42
|
+
method = ctx.get("method", "GET").upper()
|
|
43
|
+
if method != "GET":
|
|
44
|
+
return await next_fn(ctx)
|
|
45
|
+
|
|
46
|
+
url = ctx.get("url", "")
|
|
47
|
+
cached = plugin._get(url)
|
|
48
|
+
if cached is not None:
|
|
49
|
+
return cached
|
|
50
|
+
|
|
51
|
+
result = await next_fn(ctx)
|
|
52
|
+
plugin._set(url, result)
|
|
53
|
+
return result
|
|
54
|
+
|
|
55
|
+
client.use(cache_middleware)
|
|
56
|
+
|
|
57
|
+
def _get(self, key: str) -> Any | None:
|
|
58
|
+
entry = self._store.get(key)
|
|
59
|
+
if entry is None:
|
|
60
|
+
return None
|
|
61
|
+
ts, value = entry
|
|
62
|
+
if time.monotonic() - ts > self._ttl:
|
|
63
|
+
del self._store[key]
|
|
64
|
+
return None
|
|
65
|
+
return value
|
|
66
|
+
|
|
67
|
+
def _set(self, key: str, value: Any) -> None:
|
|
68
|
+
if len(self._store) >= self._max_size:
|
|
69
|
+
oldest_key = next(iter(self._store))
|
|
70
|
+
del self._store[oldest_key]
|
|
71
|
+
self._store[key] = (time.monotonic(), value)
|
|
72
|
+
|
|
73
|
+
def clear(self) -> None:
|
|
74
|
+
"""Remove all entries from the cache."""
|
|
75
|
+
self._store.clear()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from axios_python.client import AxiosPython
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"LoggerPlugin",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
_logger = logging.getLogger("axios_python")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LoggerPlugin:
|
|
17
|
+
"""Plugin that logs outgoing requests and incoming responses.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
level: The logging level to use (default: ``logging.DEBUG``).
|
|
21
|
+
logger: An optional custom logger instance.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
level: int = logging.DEBUG,
|
|
27
|
+
logger: logging.Logger | None = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
self._level = level
|
|
30
|
+
self._logger = logger or _logger
|
|
31
|
+
|
|
32
|
+
def install(self, client: AxiosPython) -> None:
|
|
33
|
+
"""Register request and response interceptors for logging.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
client: The AxiosPython client to extend.
|
|
37
|
+
"""
|
|
38
|
+
client.interceptors.request.use(self._log_request)
|
|
39
|
+
client.interceptors.response.use(self._log_response)
|
|
40
|
+
|
|
41
|
+
def _log_request(self, config: dict[str, Any]) -> dict[str, Any]:
|
|
42
|
+
self._logger.log(
|
|
43
|
+
self._level,
|
|
44
|
+
"%s %s",
|
|
45
|
+
config.get("method", "GET").upper(),
|
|
46
|
+
config.get("url", ""),
|
|
47
|
+
)
|
|
48
|
+
return config
|
|
49
|
+
|
|
50
|
+
def _log_response(self, response: Any) -> Any:
|
|
51
|
+
self._logger.log(
|
|
52
|
+
self._level,
|
|
53
|
+
"Response %s %s",
|
|
54
|
+
getattr(response, "status_code", "?"),
|
|
55
|
+
getattr(response, "request", None),
|
|
56
|
+
)
|
|
57
|
+
return response
|
axios_python/request.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"PreparedRequest",
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class PreparedRequest:
|
|
13
|
+
"""An immutable snapshot of a fully resolved HTTP request."""
|
|
14
|
+
|
|
15
|
+
method: str
|
|
16
|
+
url: str
|
|
17
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
18
|
+
params: dict[str, Any] = field(default_factory=dict)
|
|
19
|
+
data: Any = None
|
|
20
|
+
json: Any = None
|
|
21
|
+
files: Any = None
|
|
22
|
+
stream: bool = False
|
|
23
|
+
timeout: int | float = 30
|
axios_python/response.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, AsyncIterator, Iterator
|
|
4
|
+
|
|
5
|
+
from axios_python.request import PreparedRequest
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Response",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Response:
|
|
13
|
+
"""Wraps a raw HTTP response with a convenient interface.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
status_code: The HTTP status code.
|
|
17
|
+
headers: Response headers as a dict.
|
|
18
|
+
data: The decoded response body.
|
|
19
|
+
request: The original PreparedRequest that produced this response.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
status_code: int,
|
|
25
|
+
headers: dict[str, str],
|
|
26
|
+
data: Any,
|
|
27
|
+
request: PreparedRequest,
|
|
28
|
+
raw: Any = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
self.status_code = status_code
|
|
31
|
+
self.headers = headers
|
|
32
|
+
self.data = data
|
|
33
|
+
self.request = request
|
|
34
|
+
self._raw = raw
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def ok(self) -> bool:
|
|
38
|
+
"""True if the status code indicates success (2xx)."""
|
|
39
|
+
return 200 <= self.status_code < 300
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def text(self) -> str:
|
|
43
|
+
"""The response body as a string."""
|
|
44
|
+
if self.data is None:
|
|
45
|
+
try:
|
|
46
|
+
self.read()
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
if isinstance(self.data, str):
|
|
50
|
+
return self.data
|
|
51
|
+
if isinstance(self.data, bytes):
|
|
52
|
+
return self.data.decode("utf-8", errors="replace")
|
|
53
|
+
return str(self.data) if self.data is not None else ""
|
|
54
|
+
|
|
55
|
+
def json(self) -> Any:
|
|
56
|
+
"""Parse the response body as JSON.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
The parsed JSON data. If the body is already a dict or list,
|
|
60
|
+
it is returned directly.
|
|
61
|
+
"""
|
|
62
|
+
if isinstance(self.data, (dict, list)):
|
|
63
|
+
return self.data
|
|
64
|
+
import json as _json
|
|
65
|
+
return _json.loads(self.text)
|
|
66
|
+
|
|
67
|
+
def raise_for_status(self) -> Response:
|
|
68
|
+
"""Raises HTTPStatusError if one occurred.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
The response object.
|
|
72
|
+
"""
|
|
73
|
+
if not self.ok:
|
|
74
|
+
from axios_python.exceptions import HTTPStatusError
|
|
75
|
+
reason = getattr(self._raw, "reason_phrase", "Unknown Reason")
|
|
76
|
+
message = f"{self.status_code} {reason} for url: {self.request.url}"
|
|
77
|
+
raise HTTPStatusError(message, response=self)
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
def iter_bytes(self, chunk_size: int | None = None) -> Iterator[bytes]:
|
|
81
|
+
"""Iterate over the response body in bytes."""
|
|
82
|
+
return self._raw.iter_bytes(chunk_size=chunk_size)
|
|
83
|
+
|
|
84
|
+
def iter_text(self, chunk_size: int | None = None) -> Iterator[str]:
|
|
85
|
+
"""Iterate over the response body in text."""
|
|
86
|
+
return self._raw.iter_text(chunk_size=chunk_size)
|
|
87
|
+
|
|
88
|
+
def iter_lines(self) -> Iterator[str]:
|
|
89
|
+
"""Iterate over the response body line by line."""
|
|
90
|
+
return self._raw.iter_lines()
|
|
91
|
+
|
|
92
|
+
def aiter_bytes(self, chunk_size: int | None = None) -> AsyncIterator[bytes]:
|
|
93
|
+
"""Asynchronously iterate over the response body in bytes."""
|
|
94
|
+
return self._raw.aiter_bytes(chunk_size=chunk_size)
|
|
95
|
+
|
|
96
|
+
def aiter_text(self, chunk_size: int | None = None) -> AsyncIterator[str]:
|
|
97
|
+
"""Asynchronously iterate over the response body in text."""
|
|
98
|
+
return self._raw.aiter_text(chunk_size=chunk_size)
|
|
99
|
+
|
|
100
|
+
def aiter_lines(self) -> AsyncIterator[str]:
|
|
101
|
+
"""Asynchronously iterate over the response body line by line."""
|
|
102
|
+
return self._raw.aiter_lines()
|
|
103
|
+
|
|
104
|
+
def read(self) -> bytes:
|
|
105
|
+
"""Read the entire response body in bytes."""
|
|
106
|
+
self.data = self._raw.read()
|
|
107
|
+
return self.data
|
|
108
|
+
|
|
109
|
+
async def aread(self) -> bytes:
|
|
110
|
+
"""Asynchronously read the entire response body in bytes."""
|
|
111
|
+
self.data = await self._raw.aread()
|
|
112
|
+
return self.data
|
|
113
|
+
|
|
114
|
+
def close(self) -> None:
|
|
115
|
+
"""Close the underlying HTTP response stream."""
|
|
116
|
+
if self._raw is not None:
|
|
117
|
+
self._raw.close()
|
|
118
|
+
|
|
119
|
+
async def aclose(self) -> None:
|
|
120
|
+
"""Asynchronously close the underlying HTTP response stream."""
|
|
121
|
+
if self._raw is not None:
|
|
122
|
+
await self._raw.aclose()
|
|
123
|
+
|
|
124
|
+
def __enter__(self) -> Response:
|
|
125
|
+
return self
|
|
126
|
+
|
|
127
|
+
def __exit__(self, *args: Any) -> None:
|
|
128
|
+
self.close()
|
|
129
|
+
|
|
130
|
+
async def __aenter__(self) -> Response:
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
134
|
+
await self.aclose()
|
|
135
|
+
|
|
136
|
+
def __repr__(self) -> str:
|
|
137
|
+
return f"<Response [{self.status_code}]>"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__: list[str] = []
|