headgate 0.2.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.
- headgate/__init__.py +131 -0
- headgate/_attempts.py +50 -0
- headgate/_auth.py +504 -0
- headgate/_body.py +115 -0
- headgate/_client.py +927 -0
- headgate/_codec.py +24 -0
- headgate/_core/__init__.py +88 -0
- headgate/_core/_context.py +228 -0
- headgate/_core/_events.py +248 -0
- headgate/_core/_origin.py +39 -0
- headgate/_core/_retry_after.py +76 -0
- headgate/_core/_seams.py +50 -0
- headgate/_core/_switch.py +40 -0
- headgate/_core/_time.py +102 -0
- headgate/_curl.py +287 -0
- headgate/_deprecation.py +101 -0
- headgate/_failures.py +439 -0
- headgate/_middleware.py +90 -0
- headgate/_options.py +358 -0
- headgate/_redirects.py +86 -0
- headgate/_request_body.py +81 -0
- headgate/_responses.py +290 -0
- headgate/_stack.py +185 -0
- headgate/_url.py +258 -0
- headgate/_user_agent.py +30 -0
- headgate/_view.py +105 -0
- headgate/breaker/__init__.py +24 -0
- headgate/breaker/_breaker.py +302 -0
- headgate/breaker/_policy.py +37 -0
- headgate/breaker/_registry.py +168 -0
- headgate/breaker/_scope.py +43 -0
- headgate/cache/__init__.py +35 -0
- headgate/cache/_conditional.py +89 -0
- headgate/cache/_credentials.py +143 -0
- headgate/cache/_policy.py +131 -0
- headgate/cache/_transport.py +568 -0
- headgate/curl.py +8 -0
- headgate/errors.py +329 -0
- headgate/events.py +45 -0
- headgate/health.py +73 -0
- headgate/msgspec.py +125 -0
- headgate/otel.py +236 -0
- headgate/pagination/__init__.py +25 -0
- headgate/pagination/_link.py +141 -0
- headgate/pagination/_pager.py +197 -0
- headgate/pagination/_paginated.py +82 -0
- headgate/policy.py +43 -0
- headgate/py.typed +0 -0
- headgate/pydantic.py +131 -0
- headgate/ratelimit/__init__.py +23 -0
- headgate/ratelimit/_budget.py +275 -0
- headgate/ratelimit/_budgets.py +62 -0
- headgate/ratelimit/_feedback.py +133 -0
- headgate/ratelimit/_fields.py +187 -0
- headgate/ratelimit/_inflight.py +92 -0
- headgate/ratelimit/_rate.py +103 -0
- headgate/ratelimit/_transport.py +106 -0
- headgate/retry/__init__.py +40 -0
- headgate/retry/_budget.py +73 -0
- headgate/retry/_classify.py +135 -0
- headgate/retry/_policy.py +254 -0
- headgate/retry/_tracking.py +121 -0
- headgate/retry/_transport.py +448 -0
- headgate/security/__init__.py +36 -0
- headgate/security/_addresses.py +120 -0
- headgate/security/_credentials.py +124 -0
- headgate/security/_downgrade.py +42 -0
- headgate/security/_transport.py +119 -0
- headgate/streaming/__init__.py +12 -0
- headgate/streaming/_stream.py +164 -0
- headgate/testing/__init__.py +58 -0
- headgate/testing/_breakers.py +33 -0
- headgate/testing/_recording.py +120 -0
- headgate/testing/_time.py +84 -0
- headgate/testing/_transport.py +115 -0
- headgate/testing/faults.py +275 -0
- headgate-0.2.0.dist-info/METADATA +373 -0
- headgate-0.2.0.dist-info/RECORD +80 -0
- headgate-0.2.0.dist-info/WHEEL +4 -0
- headgate-0.2.0.dist-info/licenses/LICENSE +21 -0
headgate/__init__.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Copyright (c) 2026 Khaled Elsayed
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""headgate: an async HTTP base client with retry, rate limiting, circuit breaking and declared responses.
|
|
5
|
+
|
|
6
|
+
Built on ``httpx2``; Python 3.14 or newer. A client subclasses :class:`AsyncBaseClient`, configures it with a
|
|
7
|
+
:class:`Rate` (or a shared :class:`RateBudget`) and optionally :class:`TransportPolicy` and :class:`HttpOptions`, and
|
|
8
|
+
picks a codec flavor by importing ``headgate.msgspec`` or ``headgate.pydantic``. Re-exported here:
|
|
9
|
+
|
|
10
|
+
* the client and its options: :class:`AsyncBaseClient`, :data:`Method`, :class:`RequestOptions`,
|
|
11
|
+
:class:`TransportPolicy`, :class:`HttpOptions`, :class:`Timeouts`, :class:`IdempotencyKeys`, :class:`Route`,
|
|
12
|
+
:class:`Middleware`, :class:`Observer`;
|
|
13
|
+
* declared responses: :class:`Expect`, :data:`Reader` and the readers, :data:`RefusalCheck`, :class:`RefusedError`;
|
|
14
|
+
* auth: :class:`StaticHeaderAuth`, :class:`QueryParamAuth`, :class:`CachedTokenAuth`, :class:`Minted`,
|
|
15
|
+
:data:`MAX_MINT_RESPONSE_BYTES`;
|
|
16
|
+
* the policy values: retry (:class:`RetryPolicy`, :class:`Backoff`, :class:`RetryBudget`, :class:`Hedge`,
|
|
17
|
+
:data:`NO_RETRY`), the breaker (:class:`BreakerPolicy`, :class:`BreakerRegistry`), rate limiting (:class:`Rate`,
|
|
18
|
+
:class:`RateBudget`, :class:`RateRule`, :class:`Adaptive`) and the cache (:class:`HttpCache`, its filters,
|
|
19
|
+
:class:`Partition`, :func:`conditional`);
|
|
20
|
+
* streams and pages: :class:`Stream`, :meth:`AsyncBaseClient.paginate`'s :class:`Paginated` and its pagers;
|
|
21
|
+
* the errors, all under :class:`ClientError`.
|
|
22
|
+
|
|
23
|
+
The policy layers are subpackages with public paths of their own (:mod:`headgate.retry`, :mod:`headgate.breaker`,
|
|
24
|
+
:mod:`headgate.ratelimit`, :mod:`headgate.cache`, :mod:`headgate.security`, :mod:`headgate.streaming`,
|
|
25
|
+
:mod:`headgate.pagination`); events are in :mod:`headgate.events` and the testing kit in :mod:`headgate.testing`.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from headgate._auth import MAX_MINT_RESPONSE_BYTES, CachedTokenAuth, Minted, QueryParamAuth, StaticHeaderAuth
|
|
29
|
+
from headgate._client import AsyncBaseClient, Method, RequestOptions
|
|
30
|
+
from headgate._core import Observer
|
|
31
|
+
from headgate._middleware import Middleware
|
|
32
|
+
from headgate._options import HttpOptions, IdempotencyKeys, Route, Timeouts, TransportPolicy
|
|
33
|
+
from headgate._responses import Expect, Reader, RefusalCheck, RefusedError, as_bytes, event_stream, json_reader, nothing
|
|
34
|
+
from headgate.breaker import BreakerPolicy, BreakerRegistry
|
|
35
|
+
from headgate.cache import (
|
|
36
|
+
Fresh,
|
|
37
|
+
HttpCache,
|
|
38
|
+
MethodFilter,
|
|
39
|
+
NotModified,
|
|
40
|
+
Partition,
|
|
41
|
+
PathFragmentFilter,
|
|
42
|
+
StatusCodeFilter,
|
|
43
|
+
Validator,
|
|
44
|
+
conditional,
|
|
45
|
+
)
|
|
46
|
+
from headgate.errors import (
|
|
47
|
+
AuthError,
|
|
48
|
+
CircuitOpenError,
|
|
49
|
+
ClientConfigurationError,
|
|
50
|
+
ClientError,
|
|
51
|
+
ClientTimeoutError,
|
|
52
|
+
DecodeError,
|
|
53
|
+
ForbiddenDestinationError,
|
|
54
|
+
Problem,
|
|
55
|
+
RateLimitError,
|
|
56
|
+
ResponseParseError,
|
|
57
|
+
ResponseTooLargeError,
|
|
58
|
+
TransportError,
|
|
59
|
+
UpstreamStatusError,
|
|
60
|
+
)
|
|
61
|
+
from headgate.pagination import Cursor, LinkHeader, NextUrl, Offset, Pager, PageRequest, Paginated
|
|
62
|
+
from headgate.ratelimit import Adaptive, Rate, RateBudget, RateRule
|
|
63
|
+
from headgate.retry import NO_RETRY, Backoff, Hedge, RetryBudget, RetryPolicy
|
|
64
|
+
from headgate.streaming import Stream
|
|
65
|
+
|
|
66
|
+
__all__ = [
|
|
67
|
+
"MAX_MINT_RESPONSE_BYTES",
|
|
68
|
+
"NO_RETRY",
|
|
69
|
+
"Adaptive",
|
|
70
|
+
"AsyncBaseClient",
|
|
71
|
+
"AuthError",
|
|
72
|
+
"Backoff",
|
|
73
|
+
"BreakerPolicy",
|
|
74
|
+
"BreakerRegistry",
|
|
75
|
+
"CachedTokenAuth",
|
|
76
|
+
"CircuitOpenError",
|
|
77
|
+
"ClientConfigurationError",
|
|
78
|
+
"ClientError",
|
|
79
|
+
"ClientTimeoutError",
|
|
80
|
+
"Cursor",
|
|
81
|
+
"DecodeError",
|
|
82
|
+
"Expect",
|
|
83
|
+
"ForbiddenDestinationError",
|
|
84
|
+
"Fresh",
|
|
85
|
+
"Hedge",
|
|
86
|
+
"HttpCache",
|
|
87
|
+
"HttpOptions",
|
|
88
|
+
"IdempotencyKeys",
|
|
89
|
+
"LinkHeader",
|
|
90
|
+
"Method",
|
|
91
|
+
"MethodFilter",
|
|
92
|
+
"Middleware",
|
|
93
|
+
"Minted",
|
|
94
|
+
"NextUrl",
|
|
95
|
+
"NotModified",
|
|
96
|
+
"Observer",
|
|
97
|
+
"Offset",
|
|
98
|
+
"PageRequest",
|
|
99
|
+
"Pager",
|
|
100
|
+
"Paginated",
|
|
101
|
+
"Partition",
|
|
102
|
+
"PathFragmentFilter",
|
|
103
|
+
"Problem",
|
|
104
|
+
"QueryParamAuth",
|
|
105
|
+
"Rate",
|
|
106
|
+
"RateBudget",
|
|
107
|
+
"RateLimitError",
|
|
108
|
+
"RateRule",
|
|
109
|
+
"Reader",
|
|
110
|
+
"RefusalCheck",
|
|
111
|
+
"RefusedError",
|
|
112
|
+
"RequestOptions",
|
|
113
|
+
"ResponseParseError",
|
|
114
|
+
"ResponseTooLargeError",
|
|
115
|
+
"RetryBudget",
|
|
116
|
+
"RetryPolicy",
|
|
117
|
+
"Route",
|
|
118
|
+
"StaticHeaderAuth",
|
|
119
|
+
"StatusCodeFilter",
|
|
120
|
+
"Stream",
|
|
121
|
+
"Timeouts",
|
|
122
|
+
"TransportError",
|
|
123
|
+
"TransportPolicy",
|
|
124
|
+
"UpstreamStatusError",
|
|
125
|
+
"Validator",
|
|
126
|
+
"as_bytes",
|
|
127
|
+
"conditional",
|
|
128
|
+
"event_stream",
|
|
129
|
+
"json_reader",
|
|
130
|
+
"nothing",
|
|
131
|
+
]
|
headgate/_attempts.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Copyright (c) 2026 Khaled Elsayed
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""The attempt layer: reports every send of a call to the client's observers as an ``AttemptFinished`` event.
|
|
5
|
+
|
|
6
|
+
It sits directly under the retry layer (see :mod:`headgate._stack`), which begins each attempt on the call's context
|
|
7
|
+
before sending it, so the attempt number is read when the send starts; and it is installed only on a client with
|
|
8
|
+
observers.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import override
|
|
14
|
+
|
|
15
|
+
import httpx2
|
|
16
|
+
|
|
17
|
+
from headgate._core import AttemptFinished, CallContext
|
|
18
|
+
|
|
19
|
+
__all__ = ["AttemptTransport"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AttemptTransport(httpx2.AsyncBaseTransport):
|
|
23
|
+
"""Emit one ``AttemptFinished`` per send: the response head's status, or the type of what the send raised."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, *, transport: httpx2.AsyncBaseTransport) -> None:
|
|
26
|
+
"""Report the sends made through *transport*."""
|
|
27
|
+
self._transport = transport
|
|
28
|
+
|
|
29
|
+
@override
|
|
30
|
+
async def handle_async_request(self, request: httpx2.Request) -> httpx2.Response:
|
|
31
|
+
context = CallContext.of(request)
|
|
32
|
+
# Read before the send: a hedge begins another attempt while this one is in flight.
|
|
33
|
+
attempt = context.attempt
|
|
34
|
+
started = context.clock()
|
|
35
|
+
try:
|
|
36
|
+
response = await self._transport.handle_async_request(request)
|
|
37
|
+
except BaseException as exc:
|
|
38
|
+
elapsed = context.clock() - started
|
|
39
|
+
context.emit(AttemptFinished(call=context.info, attempt=attempt, error=type(exc).__name__, elapsed=elapsed))
|
|
40
|
+
raise
|
|
41
|
+
elapsed = context.clock() - started
|
|
42
|
+
context.emit(
|
|
43
|
+
AttemptFinished(call=context.info, attempt=attempt, status_code=response.status_code, elapsed=elapsed)
|
|
44
|
+
)
|
|
45
|
+
return response
|
|
46
|
+
|
|
47
|
+
@override
|
|
48
|
+
async def aclose(self) -> None:
|
|
49
|
+
"""Close the wrapped transport."""
|
|
50
|
+
await self._transport.aclose()
|