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.
Files changed (80) hide show
  1. headgate/__init__.py +131 -0
  2. headgate/_attempts.py +50 -0
  3. headgate/_auth.py +504 -0
  4. headgate/_body.py +115 -0
  5. headgate/_client.py +927 -0
  6. headgate/_codec.py +24 -0
  7. headgate/_core/__init__.py +88 -0
  8. headgate/_core/_context.py +228 -0
  9. headgate/_core/_events.py +248 -0
  10. headgate/_core/_origin.py +39 -0
  11. headgate/_core/_retry_after.py +76 -0
  12. headgate/_core/_seams.py +50 -0
  13. headgate/_core/_switch.py +40 -0
  14. headgate/_core/_time.py +102 -0
  15. headgate/_curl.py +287 -0
  16. headgate/_deprecation.py +101 -0
  17. headgate/_failures.py +439 -0
  18. headgate/_middleware.py +90 -0
  19. headgate/_options.py +358 -0
  20. headgate/_redirects.py +86 -0
  21. headgate/_request_body.py +81 -0
  22. headgate/_responses.py +290 -0
  23. headgate/_stack.py +185 -0
  24. headgate/_url.py +258 -0
  25. headgate/_user_agent.py +30 -0
  26. headgate/_view.py +105 -0
  27. headgate/breaker/__init__.py +24 -0
  28. headgate/breaker/_breaker.py +302 -0
  29. headgate/breaker/_policy.py +37 -0
  30. headgate/breaker/_registry.py +168 -0
  31. headgate/breaker/_scope.py +43 -0
  32. headgate/cache/__init__.py +35 -0
  33. headgate/cache/_conditional.py +89 -0
  34. headgate/cache/_credentials.py +143 -0
  35. headgate/cache/_policy.py +131 -0
  36. headgate/cache/_transport.py +568 -0
  37. headgate/curl.py +8 -0
  38. headgate/errors.py +329 -0
  39. headgate/events.py +45 -0
  40. headgate/health.py +73 -0
  41. headgate/msgspec.py +125 -0
  42. headgate/otel.py +236 -0
  43. headgate/pagination/__init__.py +25 -0
  44. headgate/pagination/_link.py +141 -0
  45. headgate/pagination/_pager.py +197 -0
  46. headgate/pagination/_paginated.py +82 -0
  47. headgate/policy.py +43 -0
  48. headgate/py.typed +0 -0
  49. headgate/pydantic.py +131 -0
  50. headgate/ratelimit/__init__.py +23 -0
  51. headgate/ratelimit/_budget.py +275 -0
  52. headgate/ratelimit/_budgets.py +62 -0
  53. headgate/ratelimit/_feedback.py +133 -0
  54. headgate/ratelimit/_fields.py +187 -0
  55. headgate/ratelimit/_inflight.py +92 -0
  56. headgate/ratelimit/_rate.py +103 -0
  57. headgate/ratelimit/_transport.py +106 -0
  58. headgate/retry/__init__.py +40 -0
  59. headgate/retry/_budget.py +73 -0
  60. headgate/retry/_classify.py +135 -0
  61. headgate/retry/_policy.py +254 -0
  62. headgate/retry/_tracking.py +121 -0
  63. headgate/retry/_transport.py +448 -0
  64. headgate/security/__init__.py +36 -0
  65. headgate/security/_addresses.py +120 -0
  66. headgate/security/_credentials.py +124 -0
  67. headgate/security/_downgrade.py +42 -0
  68. headgate/security/_transport.py +119 -0
  69. headgate/streaming/__init__.py +12 -0
  70. headgate/streaming/_stream.py +164 -0
  71. headgate/testing/__init__.py +58 -0
  72. headgate/testing/_breakers.py +33 -0
  73. headgate/testing/_recording.py +120 -0
  74. headgate/testing/_time.py +84 -0
  75. headgate/testing/_transport.py +115 -0
  76. headgate/testing/faults.py +275 -0
  77. headgate-0.2.0.dist-info/METADATA +373 -0
  78. headgate-0.2.0.dist-info/RECORD +80 -0
  79. headgate-0.2.0.dist-info/WHEEL +4 -0
  80. 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()