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.
@@ -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,9 @@
1
+ from __future__ import annotations
2
+
3
+ from axios_python.exceptions import CancelError
4
+
5
+ __all__ = [
6
+ "CancelledError",
7
+ ]
8
+
9
+ CancelledError = CancelError
@@ -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
@@ -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