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
axios_python/__init__.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""axios_python - A developer-experience-first HTTP client for Python.
|
|
2
|
+
|
|
3
|
+
Provides an Axios-inspired interface with interceptors, middleware,
|
|
4
|
+
retry, cancellation tokens, and a plugin system on top of httpx.
|
|
5
|
+
|
|
6
|
+
Quick start::
|
|
7
|
+
|
|
8
|
+
import axios_python
|
|
9
|
+
|
|
10
|
+
api = axios_python.create({"base_url": "https://api.example.com"})
|
|
11
|
+
response = api.get("/users")
|
|
12
|
+
print(response.data)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from axios_python.cancel.token import CancelToken
|
|
20
|
+
from axios_python.client import AxiosPython
|
|
21
|
+
from axios_python.config import RequestConfig, merge_config
|
|
22
|
+
from axios_python.exceptions import (
|
|
23
|
+
CancelError,
|
|
24
|
+
InterceptorError,
|
|
25
|
+
NetworkError,
|
|
26
|
+
AxiosPythonError,
|
|
27
|
+
RetryError,
|
|
28
|
+
TimeoutError,
|
|
29
|
+
HTTPStatusError,
|
|
30
|
+
)
|
|
31
|
+
from axios_python.plugins.auth import AuthPlugin
|
|
32
|
+
from axios_python.plugins.base import Plugin
|
|
33
|
+
from axios_python.plugins.cache import CachePlugin
|
|
34
|
+
from axios_python.plugins.logger import LoggerPlugin
|
|
35
|
+
from axios_python.request import PreparedRequest
|
|
36
|
+
from axios_python.response import Response
|
|
37
|
+
from axios_python.retry.strategy import (
|
|
38
|
+
ExponentialBackoff,
|
|
39
|
+
FixedDelay,
|
|
40
|
+
LinearBackoff,
|
|
41
|
+
RetryStrategy,
|
|
42
|
+
)
|
|
43
|
+
from axios_python.transport.base import BaseTransport
|
|
44
|
+
from axios_python.transport.httpx_adapter import HttpxTransport
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"create",
|
|
48
|
+
"AxiosPython",
|
|
49
|
+
"CancelToken",
|
|
50
|
+
"Response",
|
|
51
|
+
"PreparedRequest",
|
|
52
|
+
"RequestConfig",
|
|
53
|
+
"merge_config",
|
|
54
|
+
"AxiosPythonError",
|
|
55
|
+
"TimeoutError",
|
|
56
|
+
"NetworkError",
|
|
57
|
+
"CancelError",
|
|
58
|
+
"RetryError",
|
|
59
|
+
"InterceptorError",
|
|
60
|
+
"HTTPStatusError",
|
|
61
|
+
"Plugin",
|
|
62
|
+
"LoggerPlugin",
|
|
63
|
+
"CachePlugin",
|
|
64
|
+
"AuthPlugin",
|
|
65
|
+
"BaseTransport",
|
|
66
|
+
"HttpxTransport",
|
|
67
|
+
"RetryStrategy",
|
|
68
|
+
"FixedDelay",
|
|
69
|
+
"ExponentialBackoff",
|
|
70
|
+
"LinearBackoff",
|
|
71
|
+
"request",
|
|
72
|
+
"get",
|
|
73
|
+
"post",
|
|
74
|
+
"put",
|
|
75
|
+
"patch",
|
|
76
|
+
"delete",
|
|
77
|
+
"async_request",
|
|
78
|
+
"async_get",
|
|
79
|
+
"async_post",
|
|
80
|
+
"async_put",
|
|
81
|
+
"async_patch",
|
|
82
|
+
"async_delete",
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def create(config: dict[str, Any] | None = None, **kwargs: Any) -> AxiosPython:
|
|
87
|
+
"""Create a new axios_python client instance.
|
|
88
|
+
|
|
89
|
+
This is the primary entry point for the library.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
config: A configuration dict with keys like ``base_url``,
|
|
93
|
+
``timeout``, ``headers``, ``params``, ``max_retries``, etc.
|
|
94
|
+
**kwargs: Additional config keys merged into *config*.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
A configured :class:`AxiosPython` client instance.
|
|
98
|
+
|
|
99
|
+
Example::
|
|
100
|
+
|
|
101
|
+
api = axios_python.create({
|
|
102
|
+
"base_url": "https://api.example.com",
|
|
103
|
+
"timeout": 10,
|
|
104
|
+
})
|
|
105
|
+
response = api.get("/users")
|
|
106
|
+
"""
|
|
107
|
+
merged = dict(config or {})
|
|
108
|
+
merged.update(kwargs)
|
|
109
|
+
return AxiosPython(config=merged)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
_default_instance = create()
|
|
113
|
+
|
|
114
|
+
def request(method: str, url: str, **kwargs: Any) -> Response:
|
|
115
|
+
return _default_instance.request(method, url, **kwargs)
|
|
116
|
+
|
|
117
|
+
def get(url: str, **kwargs: Any) -> Response:
|
|
118
|
+
return _default_instance.get(url, **kwargs)
|
|
119
|
+
|
|
120
|
+
def post(url: str, **kwargs: Any) -> Response:
|
|
121
|
+
return _default_instance.post(url, **kwargs)
|
|
122
|
+
|
|
123
|
+
def put(url: str, **kwargs: Any) -> Response:
|
|
124
|
+
return _default_instance.put(url, **kwargs)
|
|
125
|
+
|
|
126
|
+
def patch(url: str, **kwargs: Any) -> Response:
|
|
127
|
+
return _default_instance.patch(url, **kwargs)
|
|
128
|
+
|
|
129
|
+
def delete(url: str, **kwargs: Any) -> Response:
|
|
130
|
+
return _default_instance.delete(url, **kwargs)
|
|
131
|
+
|
|
132
|
+
async def async_request(method: str, url: str, **kwargs: Any) -> Response:
|
|
133
|
+
return await _default_instance.async_request(method, url, **kwargs)
|
|
134
|
+
|
|
135
|
+
async def async_get(url: str, **kwargs: Any) -> Response:
|
|
136
|
+
return await _default_instance.async_get(url, **kwargs)
|
|
137
|
+
|
|
138
|
+
async def async_post(url: str, **kwargs: Any) -> Response:
|
|
139
|
+
return await _default_instance.async_post(url, **kwargs)
|
|
140
|
+
|
|
141
|
+
async def async_put(url: str, **kwargs: Any) -> Response:
|
|
142
|
+
return await _default_instance.async_put(url, **kwargs)
|
|
143
|
+
|
|
144
|
+
async def async_patch(url: str, **kwargs: Any) -> Response:
|
|
145
|
+
return await _default_instance.async_patch(url, **kwargs)
|
|
146
|
+
|
|
147
|
+
async def async_delete(url: str, **kwargs: Any) -> Response:
|
|
148
|
+
return await _default_instance.async_delete(url, **kwargs)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
from axios_python.exceptions import CancelError
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"CancelToken",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CancelToken:
|
|
13
|
+
"""A token used to signal request cancellation.
|
|
14
|
+
|
|
15
|
+
Create a token and pass it to a request via the ``cancel_token``
|
|
16
|
+
config key. Call :meth:`cancel` at any time to abort the associated
|
|
17
|
+
request.
|
|
18
|
+
|
|
19
|
+
Example::
|
|
20
|
+
|
|
21
|
+
token = CancelToken()
|
|
22
|
+
api.get("/slow", cancel_token=token)
|
|
23
|
+
token.cancel()
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
self._cancelled: bool = False
|
|
28
|
+
self._reason: str = "Request cancelled"
|
|
29
|
+
self._callbacks: list[Callable[[], None]] = []
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def is_cancelled(self) -> bool:
|
|
33
|
+
"""Return True if the token has been cancelled."""
|
|
34
|
+
return self._cancelled
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def reason(self) -> str:
|
|
38
|
+
"""Return the cancellation reason string."""
|
|
39
|
+
return self._reason
|
|
40
|
+
|
|
41
|
+
def cancel(self, reason: str = "Request cancelled") -> None:
|
|
42
|
+
"""Cancel the token, firing all registered callbacks.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
reason: A human-readable reason for the cancellation.
|
|
46
|
+
"""
|
|
47
|
+
if self._cancelled:
|
|
48
|
+
return
|
|
49
|
+
self._cancelled = True
|
|
50
|
+
self._reason = reason
|
|
51
|
+
for cb in self._callbacks:
|
|
52
|
+
cb()
|
|
53
|
+
|
|
54
|
+
def on_cancel(self, callback: Callable[[], None]) -> None:
|
|
55
|
+
"""Register a callback to fire when the token is cancelled.
|
|
56
|
+
|
|
57
|
+
If the token is already cancelled, the callback fires immediately.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
callback: A zero-argument callable.
|
|
61
|
+
"""
|
|
62
|
+
if self._cancelled:
|
|
63
|
+
callback()
|
|
64
|
+
return
|
|
65
|
+
self._callbacks.append(callback)
|
|
66
|
+
|
|
67
|
+
def raise_if_cancelled(self) -> None:
|
|
68
|
+
"""Raise ``CancelError`` if the token has been cancelled.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
CancelError: If the token was previously cancelled.
|
|
72
|
+
"""
|
|
73
|
+
if self._cancelled:
|
|
74
|
+
raise CancelError(self._reason)
|
axios_python/client.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Awaitable, Callable
|
|
4
|
+
|
|
5
|
+
from axios_python.cancel.token import CancelToken
|
|
6
|
+
from axios_python.config import merge_config
|
|
7
|
+
from axios_python.interceptors.manager import InterceptorManager
|
|
8
|
+
from axios_python.middleware.manager import MiddlewareManager
|
|
9
|
+
from axios_python.plugins.base import Plugin
|
|
10
|
+
from axios_python.request import PreparedRequest
|
|
11
|
+
from axios_python.response import Response
|
|
12
|
+
from axios_python.retry.engine import RetryEngine
|
|
13
|
+
from axios_python.retry.strategy import RetryStrategy
|
|
14
|
+
from axios_python.transport.base import BaseTransport
|
|
15
|
+
from axios_python.transport.httpx_adapter import HttpxTransport
|
|
16
|
+
from axios_python.utils.async_utils import run_sync
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"AxiosPython",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
MiddlewareFn = Callable[[dict[str, Any], Callable[..., Awaitable[Any]]], Awaitable[Any]]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AxiosPython:
|
|
26
|
+
"""The main axios_python HTTP client.
|
|
27
|
+
|
|
28
|
+
Each instance maintains its own configuration, interceptors, middleware
|
|
29
|
+
stack, and transport. Create instances via :func:`axios_python.create`.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
config: Base configuration dict applied to every request made
|
|
33
|
+
through this instance.
|
|
34
|
+
transport: An optional custom transport adapter. Defaults to
|
|
35
|
+
:class:`~axios_python.transport.httpx_adapter.HttpxTransport`.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
config: dict[str, Any] | None = None,
|
|
41
|
+
transport: BaseTransport | None = None,
|
|
42
|
+
) -> None:
|
|
43
|
+
self._config: dict[str, Any] = config or {}
|
|
44
|
+
self._transport: BaseTransport = transport or HttpxTransport()
|
|
45
|
+
self._interceptors: InterceptorManager = InterceptorManager()
|
|
46
|
+
self._middleware: MiddlewareManager = MiddlewareManager()
|
|
47
|
+
self._plugins: list[Plugin] = []
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def interceptors(self) -> InterceptorManager:
|
|
51
|
+
"""Access request and response interceptor chains."""
|
|
52
|
+
return self._interceptors
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def defaults(self) -> dict[str, Any]:
|
|
56
|
+
"""The base configuration for this client instance."""
|
|
57
|
+
return self._config
|
|
58
|
+
|
|
59
|
+
def use(self, middleware: MiddlewareFn) -> AxiosPython:
|
|
60
|
+
"""Register a middleware function.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
middleware: An async callable with signature
|
|
64
|
+
``(ctx, next) -> Any``.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
This client instance for chaining.
|
|
68
|
+
"""
|
|
69
|
+
self._middleware.use(middleware)
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
def plugin(self, p: Plugin) -> AxiosPython:
|
|
73
|
+
"""Install a plugin onto this client.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
p: A plugin implementing the :class:`~axios_python.plugins.base.Plugin`
|
|
77
|
+
protocol.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
This client instance for chaining.
|
|
81
|
+
"""
|
|
82
|
+
p.install(self)
|
|
83
|
+
self._plugins.append(p)
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
def _build_request_config(
|
|
87
|
+
self,
|
|
88
|
+
method: str,
|
|
89
|
+
url: str,
|
|
90
|
+
**kwargs: Any,
|
|
91
|
+
) -> dict[str, Any]:
|
|
92
|
+
per_request: dict[str, Any] = {"method": method, "url": url}
|
|
93
|
+
per_request.update(kwargs)
|
|
94
|
+
return merge_config(self._config, per_request)
|
|
95
|
+
|
|
96
|
+
def _resolve_url(self, config: dict[str, Any]) -> str:
|
|
97
|
+
base = config.get("base_url", "").rstrip("/")
|
|
98
|
+
path = config.get("url", "")
|
|
99
|
+
if path.startswith(("http://", "https://")):
|
|
100
|
+
return path
|
|
101
|
+
return f"{base}/{path.lstrip('/')}" if base else path
|
|
102
|
+
|
|
103
|
+
def _prepare_request(self, config: dict[str, Any]) -> PreparedRequest:
|
|
104
|
+
return PreparedRequest(
|
|
105
|
+
method=config.get("method", "GET").upper(),
|
|
106
|
+
url=self._resolve_url(config),
|
|
107
|
+
headers=dict(config.get("headers", {})),
|
|
108
|
+
params=dict(config.get("params", {})),
|
|
109
|
+
data=config.get("data"),
|
|
110
|
+
json=config.get("json"),
|
|
111
|
+
files=config.get("files"),
|
|
112
|
+
stream=config.get("stream", False),
|
|
113
|
+
timeout=config.get("timeout", 30),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def _build_retry_engine(self, config: dict[str, Any]) -> RetryEngine:
|
|
117
|
+
return RetryEngine(
|
|
118
|
+
max_retries=config.get("max_retries", 0),
|
|
119
|
+
strategy=config.get("retry_strategy"),
|
|
120
|
+
retry_on=config.get("retry_on"),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def _execute_sync(self, config: dict[str, Any]) -> Response:
|
|
124
|
+
cancel_token: CancelToken | None = config.get("cancel_token")
|
|
125
|
+
|
|
126
|
+
if cancel_token is not None:
|
|
127
|
+
cancel_token.raise_if_cancelled()
|
|
128
|
+
|
|
129
|
+
config = self._interceptors.request.run(config)
|
|
130
|
+
|
|
131
|
+
prepared = self._prepare_request(config)
|
|
132
|
+
retry = self._build_retry_engine(config)
|
|
133
|
+
|
|
134
|
+
def transport_call() -> Response:
|
|
135
|
+
if cancel_token is not None:
|
|
136
|
+
cancel_token.raise_if_cancelled()
|
|
137
|
+
return self._transport.send(prepared)
|
|
138
|
+
|
|
139
|
+
response = retry.execute(transport_call)
|
|
140
|
+
response = self._interceptors.response.run(response)
|
|
141
|
+
return response
|
|
142
|
+
|
|
143
|
+
async def _execute_async(self, config: dict[str, Any]) -> Response:
|
|
144
|
+
cancel_token: CancelToken | None = config.get("cancel_token")
|
|
145
|
+
|
|
146
|
+
if cancel_token is not None:
|
|
147
|
+
cancel_token.raise_if_cancelled()
|
|
148
|
+
|
|
149
|
+
config = await self._interceptors.request.run_async(config)
|
|
150
|
+
|
|
151
|
+
prepared = self._prepare_request(config)
|
|
152
|
+
retry = self._build_retry_engine(config)
|
|
153
|
+
|
|
154
|
+
async def transport_call() -> Response:
|
|
155
|
+
if cancel_token is not None:
|
|
156
|
+
cancel_token.raise_if_cancelled()
|
|
157
|
+
return await self._transport.send_async(prepared)
|
|
158
|
+
|
|
159
|
+
response = await retry.execute_async(transport_call)
|
|
160
|
+
response = await self._interceptors.response.run_async(response)
|
|
161
|
+
return response
|
|
162
|
+
|
|
163
|
+
async def _dispatch_async(self, config: dict[str, Any]) -> Response:
|
|
164
|
+
if len(self._middleware) > 0:
|
|
165
|
+
async def final(ctx: dict[str, Any]) -> Response:
|
|
166
|
+
return await self._execute_async(ctx)
|
|
167
|
+
return await self._middleware.execute(config, final)
|
|
168
|
+
return await self._execute_async(config)
|
|
169
|
+
|
|
170
|
+
def _dispatch_sync(self, config: dict[str, Any]) -> Response:
|
|
171
|
+
if config.get("stream"):
|
|
172
|
+
if len(self._middleware) > 0:
|
|
173
|
+
raise RuntimeError("Async middleware cannot be used with synchronous stream=True requests. Use async_get() instead.")
|
|
174
|
+
return self._execute_sync(config)
|
|
175
|
+
return run_sync(self._dispatch_async(config))
|
|
176
|
+
|
|
177
|
+
def request(self, method: str, url: str, **kwargs: Any) -> Response:
|
|
178
|
+
"""Send a synchronous HTTP request.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
method: The HTTP method (GET, POST, PUT, etc.).
|
|
182
|
+
url: The URL path or full URL.
|
|
183
|
+
**kwargs: Additional config overrides (headers, params, data,
|
|
184
|
+
json, timeout, cancel_token, etc.).
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
A :class:`~axios_python.response.Response` object.
|
|
188
|
+
"""
|
|
189
|
+
config = self._build_request_config(method, url, **kwargs)
|
|
190
|
+
return self._dispatch_sync(config)
|
|
191
|
+
|
|
192
|
+
async def async_request(self, method: str, url: str, **kwargs: Any) -> Response:
|
|
193
|
+
"""Send an asynchronous HTTP request.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
method: The HTTP method (GET, POST, PUT, etc.).
|
|
197
|
+
url: The URL path or full URL.
|
|
198
|
+
**kwargs: Additional config overrides (headers, params, data,
|
|
199
|
+
json, timeout, cancel_token, etc.).
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
A :class:`~axios_python.response.Response` object.
|
|
203
|
+
"""
|
|
204
|
+
config = self._build_request_config(method, url, **kwargs)
|
|
205
|
+
return await self._dispatch_async(config)
|
|
206
|
+
|
|
207
|
+
def get(self, url: str, **kwargs: Any) -> Response:
|
|
208
|
+
"""Send a synchronous GET request.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
url: The URL path or full URL.
|
|
212
|
+
**kwargs: Additional config overrides.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
A :class:`~axios_python.response.Response` object.
|
|
216
|
+
"""
|
|
217
|
+
return self.request("GET", url, **kwargs)
|
|
218
|
+
|
|
219
|
+
async def async_get(self, url: str, **kwargs: Any) -> Response:
|
|
220
|
+
"""Send an asynchronous GET request.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
url: The URL path or full URL.
|
|
224
|
+
**kwargs: Additional config overrides.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
A :class:`~axios_python.response.Response` object.
|
|
228
|
+
"""
|
|
229
|
+
return await self.async_request("GET", url, **kwargs)
|
|
230
|
+
|
|
231
|
+
def post(self, url: str, **kwargs: Any) -> Response:
|
|
232
|
+
"""Send a synchronous POST request.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
url: The URL path or full URL.
|
|
236
|
+
**kwargs: Additional config overrides.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
A :class:`~axios_python.response.Response` object.
|
|
240
|
+
"""
|
|
241
|
+
return self.request("POST", url, **kwargs)
|
|
242
|
+
|
|
243
|
+
async def async_post(self, url: str, **kwargs: Any) -> Response:
|
|
244
|
+
"""Send an asynchronous POST request.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
url: The URL path or full URL.
|
|
248
|
+
**kwargs: Additional config overrides.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
A :class:`~axios_python.response.Response` object.
|
|
252
|
+
"""
|
|
253
|
+
return await self.async_request("POST", url, **kwargs)
|
|
254
|
+
|
|
255
|
+
def put(self, url: str, **kwargs: Any) -> Response:
|
|
256
|
+
"""Send a synchronous PUT request.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
url: The URL path or full URL.
|
|
260
|
+
**kwargs: Additional config overrides.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
A :class:`~axios_python.response.Response` object.
|
|
264
|
+
"""
|
|
265
|
+
return self.request("PUT", url, **kwargs)
|
|
266
|
+
|
|
267
|
+
async def async_put(self, url: str, **kwargs: Any) -> Response:
|
|
268
|
+
"""Send an asynchronous PUT request.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
url: The URL path or full URL.
|
|
272
|
+
**kwargs: Additional config overrides.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
A :class:`~axios_python.response.Response` object.
|
|
276
|
+
"""
|
|
277
|
+
return await self.async_request("PUT", url, **kwargs)
|
|
278
|
+
|
|
279
|
+
def patch(self, url: str, **kwargs: Any) -> Response:
|
|
280
|
+
"""Send a synchronous PATCH request.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
url: The URL path or full URL.
|
|
284
|
+
**kwargs: Additional config overrides.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
A :class:`~axios_python.response.Response` object.
|
|
288
|
+
"""
|
|
289
|
+
return self.request("PATCH", url, **kwargs)
|
|
290
|
+
|
|
291
|
+
async def async_patch(self, url: str, **kwargs: Any) -> Response:
|
|
292
|
+
"""Send an asynchronous PATCH request.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
url: The URL path or full URL.
|
|
296
|
+
**kwargs: Additional config overrides.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
A :class:`~axios_python.response.Response` object.
|
|
300
|
+
"""
|
|
301
|
+
return await self.async_request("PATCH", url, **kwargs)
|
|
302
|
+
|
|
303
|
+
def delete(self, url: str, **kwargs: Any) -> Response:
|
|
304
|
+
"""Send a synchronous DELETE request.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
url: The URL path or full URL.
|
|
308
|
+
**kwargs: Additional config overrides.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
A :class:`~axios_python.response.Response` object.
|
|
312
|
+
"""
|
|
313
|
+
return self.request("DELETE", url, **kwargs)
|
|
314
|
+
|
|
315
|
+
async def async_delete(self, url: str, **kwargs: Any) -> Response:
|
|
316
|
+
"""Send an asynchronous DELETE request.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
url: The URL path or full URL.
|
|
320
|
+
**kwargs: Additional config overrides.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
A :class:`~axios_python.response.Response` object.
|
|
324
|
+
"""
|
|
325
|
+
return await self.async_request("DELETE", url, **kwargs)
|
|
326
|
+
|
|
327
|
+
def close(self) -> None:
|
|
328
|
+
"""Release all resources held by this client."""
|
|
329
|
+
self._transport.close()
|
|
330
|
+
|
|
331
|
+
async def aclose(self) -> None:
|
|
332
|
+
"""Release all resources held by this client asynchronously."""
|
|
333
|
+
await self._transport.aclose()
|
|
334
|
+
|
|
335
|
+
def __repr__(self) -> str:
|
|
336
|
+
base = self._config.get("base_url", "")
|
|
337
|
+
return f"<AxiosPython base_url={base!r}>"
|
|
338
|
+
|
|
339
|
+
def __enter__(self) -> AxiosPython:
|
|
340
|
+
return self
|
|
341
|
+
|
|
342
|
+
def __exit__(self, *args: Any) -> None:
|
|
343
|
+
self.close()
|
|
344
|
+
|
|
345
|
+
async def __aenter__(self) -> AxiosPython:
|
|
346
|
+
return self
|
|
347
|
+
|
|
348
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
349
|
+
await self.aclose()
|
axios_python/config.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, TypedDict
|
|
4
|
+
|
|
5
|
+
from axios_python.defaults import DEFAULT_CONFIG
|
|
6
|
+
from axios_python.utils.merge import deep_merge
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"RequestConfig",
|
|
10
|
+
"merge_config",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RequestConfig(TypedDict, total=False):
|
|
15
|
+
"""Configuration options for a single request."""
|
|
16
|
+
|
|
17
|
+
method: str
|
|
18
|
+
url: str
|
|
19
|
+
base_url: str
|
|
20
|
+
headers: dict[str, str]
|
|
21
|
+
params: dict[str, Any]
|
|
22
|
+
data: Any
|
|
23
|
+
json: Any
|
|
24
|
+
files: Any
|
|
25
|
+
stream: bool
|
|
26
|
+
timeout: int | float
|
|
27
|
+
max_retries: int
|
|
28
|
+
retry_strategy: Any
|
|
29
|
+
retry_on: Any
|
|
30
|
+
cancel_token: Any
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def merge_config(*configs: dict[str, Any]) -> dict[str, Any]:
|
|
34
|
+
"""Merge multiple configuration dicts with later values taking precedence.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
*configs: Configuration dictionaries to merge, ordered from lowest
|
|
38
|
+
to highest priority.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
A new merged configuration dictionary.
|
|
42
|
+
"""
|
|
43
|
+
result: dict[str, Any] = deep_merge({}, DEFAULT_CONFIG)
|
|
44
|
+
for cfg in configs:
|
|
45
|
+
if cfg:
|
|
46
|
+
result = deep_merge(result, cfg)
|
|
47
|
+
return result
|
axios_python/defaults.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"DEFAULT_TIMEOUT",
|
|
7
|
+
"DEFAULT_HEADERS",
|
|
8
|
+
"DEFAULT_MAX_RETRIES",
|
|
9
|
+
"DEFAULT_CONFIG",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
DEFAULT_TIMEOUT: int | float = 30
|
|
13
|
+
|
|
14
|
+
DEFAULT_HEADERS: dict[str, str] = {
|
|
15
|
+
"Accept": "application/json, text/plain, */*",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
DEFAULT_MAX_RETRIES: int = 0
|
|
19
|
+
|
|
20
|
+
DEFAULT_CONFIG: dict[str, Any] = {
|
|
21
|
+
"base_url": "",
|
|
22
|
+
"timeout": DEFAULT_TIMEOUT,
|
|
23
|
+
"headers": dict(DEFAULT_HEADERS),
|
|
24
|
+
"params": {},
|
|
25
|
+
"max_retries": DEFAULT_MAX_RETRIES,
|
|
26
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"AxiosPythonError",
|
|
5
|
+
"TimeoutError",
|
|
6
|
+
"NetworkError",
|
|
7
|
+
"CancelError",
|
|
8
|
+
"RetryError",
|
|
9
|
+
"InterceptorError",
|
|
10
|
+
"HTTPStatusError",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AxiosPythonError(Exception):
|
|
15
|
+
"""Base exception for all axios_python errors."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TimeoutError(AxiosPythonError):
|
|
19
|
+
"""Raised when a request exceeds the configured timeout."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NetworkError(AxiosPythonError):
|
|
23
|
+
"""Raised when a network-level failure occurs."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CancelError(AxiosPythonError):
|
|
27
|
+
"""Raised when a request is cancelled via a CancelToken."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class RetryError(AxiosPythonError):
|
|
31
|
+
"""Raised when all retry attempts have been exhausted."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, message: str, last_exception: Exception | None = None) -> None:
|
|
34
|
+
super().__init__(message)
|
|
35
|
+
self.last_exception = last_exception
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class InterceptorError(AxiosPythonError):
|
|
39
|
+
"""Raised when an interceptor fails during execution."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class HTTPStatusError(AxiosPythonError):
|
|
43
|
+
"""Raised when a response indicates an HTTP error (4xx or 5xx)."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, message: str, response: Any) -> None:
|
|
46
|
+
super().__init__(message)
|
|
47
|
+
self.response = response
|