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 @@
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
@@ -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
@@ -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] = []