lothc 0.2.1__tar.gz → 0.2.2__tar.gz
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.
- {lothc-0.2.1 → lothc-0.2.2}/PKG-INFO +2 -2
- {lothc-0.2.1 → lothc-0.2.2}/README.md +1 -1
- {lothc-0.2.1 → lothc-0.2.2}/lothc/_client.py +172 -18
- {lothc-0.2.1 → lothc-0.2.2}/.gitignore +0 -0
- {lothc-0.2.1 → lothc-0.2.2}/LICENSE +0 -0
- {lothc-0.2.1 → lothc-0.2.2}/lothc/__init__.py +0 -0
- {lothc-0.2.1 → lothc-0.2.2}/lothc/_compat.py +0 -0
- {lothc-0.2.1 → lothc-0.2.2}/lothc/_oauth.py +0 -0
- {lothc-0.2.1 → lothc-0.2.2}/lothc/py.typed +0 -0
- {lothc-0.2.1 → lothc-0.2.2}/lothc/testing.py +0 -0
- {lothc-0.2.1 → lothc-0.2.2}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: lothc
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Lord Of The Http Clients — a typed HTTP client built on pyreqwest, with first-class pydantic and msgspec support
|
|
5
5
|
Project-URL: Homepage, https://github.com/RogerThomas/lothc
|
|
6
6
|
Project-URL: Repository, https://github.com/RogerThomas/lothc
|
|
@@ -111,7 +111,7 @@ validate yourself. Any type expression works, so `into=list[Pokemon]` decodes a
|
|
|
111
111
|
| | |
|
|
112
112
|
|---|---|
|
|
113
113
|
| **Two clients, one API** | `HTTPClient` (async) and `SyncHTTPClient` (sync) — identical surface, both backed by pyreqwest. |
|
|
114
|
-
| **Every verb** | `get`, `post`, `put`, `patch`, `delete`, `head`, `download` (each returning a `Response`: `.data`, `.status`, `.headers`) — see [Verbs](https://rogerthomas.github.io/lothc/verbs/). |
|
|
114
|
+
| **Every verb** | `get`, `post`, `put`, `patch`, `delete`, `head`, `options`, `download` (each returning a `Response`: `.data`, `.status`, `.headers`) — see [Verbs](https://rogerthomas.github.io/lothc/verbs/). |
|
|
115
115
|
| **Typed params, headers & forms** | A `BaseModel`/`Struct` for query params or headers (with `None`-field omission), or a body sent urlencoded (`data=`) or as a real multipart form (`form=`). |
|
|
116
116
|
| **Precise static types** | Every verb is paired `@overload`s, not a cast-laden generic — your editor knows the exact return type. |
|
|
117
117
|
| **SSE & streaming** | Spec-compliant SSE — automatic reconnect with `Last-Event-ID`, server `retry:` honored, typed decode (discriminated unions included) — plus raw or NDJSON-typed `stream_get`/`stream_post` — see [SSE](https://rogerthomas.github.io/lothc/sse/) / [Streaming](https://rogerthomas.github.io/lothc/streaming/). |
|
|
@@ -84,7 +84,7 @@ validate yourself. Any type expression works, so `into=list[Pokemon]` decodes a
|
|
|
84
84
|
| | |
|
|
85
85
|
|---|---|
|
|
86
86
|
| **Two clients, one API** | `HTTPClient` (async) and `SyncHTTPClient` (sync) — identical surface, both backed by pyreqwest. |
|
|
87
|
-
| **Every verb** | `get`, `post`, `put`, `patch`, `delete`, `head`, `download` (each returning a `Response`: `.data`, `.status`, `.headers`) — see [Verbs](https://rogerthomas.github.io/lothc/verbs/). |
|
|
87
|
+
| **Every verb** | `get`, `post`, `put`, `patch`, `delete`, `head`, `options`, `download` (each returning a `Response`: `.data`, `.status`, `.headers`) — see [Verbs](https://rogerthomas.github.io/lothc/verbs/). |
|
|
88
88
|
| **Typed params, headers & forms** | A `BaseModel`/`Struct` for query params or headers (with `None`-field omission), or a body sent urlencoded (`data=`) or as a real multipart form (`form=`). |
|
|
89
89
|
| **Precise static types** | Every verb is paired `@overload`s, not a cast-laden generic — your editor knows the exact return type. |
|
|
90
90
|
| **SSE & streaming** | Spec-compliant SSE — automatic reconnect with `Last-Event-ID`, server `retry:` honored, typed decode (discriminated unions included) — plus raw or NDJSON-typed `stream_get`/`stream_post` — see [SSE](https://rogerthomas.github.io/lothc/sse/) / [Streaming](https://rogerthomas.github.io/lothc/streaming/). |
|
|
@@ -167,9 +167,9 @@ type SyncAuthProvider = Callable[[], str]
|
|
|
167
167
|
|
|
168
168
|
@dataclass(slots=True)
|
|
169
169
|
class RequestInfo:
|
|
170
|
-
"""The request actually sent, attached to `Response.request
|
|
171
|
-
`HTTPResponseError.request` — the target you asked for, not necessarily the
|
|
172
|
-
response came from if redirects were followed.
|
|
170
|
+
"""The request actually sent, attached to `Response.request` (every non-streaming verb's
|
|
171
|
+
result) and to `HTTPResponseError.request` — the target you asked for, not necessarily the
|
|
172
|
+
one a final response came from if redirects were followed.
|
|
173
173
|
"""
|
|
174
174
|
|
|
175
175
|
method: str
|
|
@@ -2366,7 +2366,7 @@ def _parse_typed_headers(
|
|
|
2366
2366
|
|
|
2367
2367
|
@dataclass
|
|
2368
2368
|
class Response[TData, THeaders: TypedHeaders | None = None]:
|
|
2369
|
-
"""What `get`/`post`/`put`/`patch`/`delete`/`head`/`download` return: the decoded body
|
|
2369
|
+
"""What `get`/`post`/`put`/`patch`/`delete`/`head`/`options`/`download` return: the decoded body
|
|
2370
2370
|
(`.data`) alongside the status and headers.
|
|
2371
2371
|
|
|
2372
2372
|
`.typed_headers` is `None` unless `response_headers_type` was passed to the call that
|
|
@@ -2561,7 +2561,13 @@ class HTTPClient:
|
|
|
2561
2561
|
See `SyncHTTPClient` for the sync mirror — same methods, same overload shapes.
|
|
2562
2562
|
"""
|
|
2563
2563
|
|
|
2564
|
-
_default_retry_methods: ClassVar[frozenset[str]] = frozenset({
|
|
2564
|
+
_default_retry_methods: ClassVar[frozenset[str]] = frozenset({
|
|
2565
|
+
"GET",
|
|
2566
|
+
"PUT",
|
|
2567
|
+
"DELETE",
|
|
2568
|
+
"HEAD",
|
|
2569
|
+
"OPTIONS",
|
|
2570
|
+
})
|
|
2565
2571
|
# The streaming verbs (`sse`, `stream_get`, `stream_post`, `download`) swap the client's total
|
|
2566
2572
|
# `timeout` for this per-request one whenever the caller doesn't pass their own: pyreqwest has
|
|
2567
2573
|
# no "no timeout" override, and a total timeout on a long-lived body kills every healthy
|
|
@@ -2629,12 +2635,13 @@ class HTTPClient:
|
|
|
2629
2635
|
`bearer_token` is a static token; `bearer_auth` is an async callable resolved fresh on
|
|
2630
2636
|
every request; `basic_auth` is a `(username, password)` pair — provide at most one of the
|
|
2631
2637
|
three. `max_retries` enables a real retry middleware (backoff, `Retry-After`-aware); with
|
|
2632
|
-
no `retry_methods`, only the idempotent verbs (`GET`/`PUT`/`DELETE`/`HEAD`)
|
|
2633
|
-
`backoff_base` scales the wait between attempts (`backoff_base * 2 **
|
|
2634
|
-
0.1 default waits 0.1s then 0.2s) — a `Retry-After` header on the
|
|
2635
|
-
computed delay, up to `max_retry_after` (60s by default): a server
|
|
2636
|
-
ends retrying, and its 429/503 is raised as `HTTPResponseError`
|
|
2637
|
-
from `e.headers["Retry-After"]`. Pass `max_retry_after=None`
|
|
2638
|
+
no `retry_methods`, only the idempotent verbs (`GET`/`PUT`/`DELETE`/`HEAD`/`OPTIONS`)
|
|
2639
|
+
retry, and `backoff_base` scales the wait between attempts (`backoff_base * 2 **
|
|
2640
|
+
attempt`, so the 0.1 default waits 0.1s then 0.2s) — a `Retry-After` header on the
|
|
2641
|
+
response wins over the computed delay, up to `max_retry_after` (60s by default): a server
|
|
2642
|
+
asking for a longer wait ends retrying, and its 429/503 is raised as `HTTPResponseError`
|
|
2643
|
+
so you can schedule a retry from `e.headers["Retry-After"]`. Pass `max_retry_after=None`
|
|
2644
|
+
to honour any wait.
|
|
2638
2645
|
|
|
2639
2646
|
`connect_timeout` bounds only the TCP connect phase (separate from `timeout`, which
|
|
2640
2647
|
covers the whole request); `read_timeout` bounds the idle gap between two consecutive
|
|
@@ -4450,6 +4457,76 @@ class HTTPClient:
|
|
|
4450
4457
|
elapsed,
|
|
4451
4458
|
)
|
|
4452
4459
|
|
|
4460
|
+
@overload
|
|
4461
|
+
async def options(
|
|
4462
|
+
self,
|
|
4463
|
+
path: str,
|
|
4464
|
+
*,
|
|
4465
|
+
params: Params | None = None,
|
|
4466
|
+
headers: Headers | None = None,
|
|
4467
|
+
timeout: float | None = None,
|
|
4468
|
+
skip_auth: bool = False,
|
|
4469
|
+
error_for_status: bool = True,
|
|
4470
|
+
raises: type[HTTPResponseError[Any]] | None = None,
|
|
4471
|
+
) -> Response[None]: ...
|
|
4472
|
+
@overload
|
|
4473
|
+
async def options[THeaders: TypedHeaders](
|
|
4474
|
+
self,
|
|
4475
|
+
path: str,
|
|
4476
|
+
*,
|
|
4477
|
+
params: Params | None = None,
|
|
4478
|
+
headers: Headers | None = None,
|
|
4479
|
+
timeout: float | None = None,
|
|
4480
|
+
skip_auth: bool = False,
|
|
4481
|
+
response_headers_type: type[THeaders],
|
|
4482
|
+
error_for_status: bool = True,
|
|
4483
|
+
raises: type[HTTPResponseError[Any]] | None = None,
|
|
4484
|
+
) -> Response[None, THeaders]: ...
|
|
4485
|
+
async def options(
|
|
4486
|
+
self,
|
|
4487
|
+
path: str,
|
|
4488
|
+
*,
|
|
4489
|
+
params: Params | None = None,
|
|
4490
|
+
headers: Headers | None = None,
|
|
4491
|
+
timeout: float | None = None,
|
|
4492
|
+
skip_auth: bool = False,
|
|
4493
|
+
response_headers_type: type[TypedHeaders] | None = None,
|
|
4494
|
+
error_for_status: bool = True,
|
|
4495
|
+
raises: type[HTTPResponseError[Any]] | None = None,
|
|
4496
|
+
) -> Response[None, Any]:
|
|
4497
|
+
"""OPTIONS `path` (e.g. a CORS preflight) — status and headers only, any body is discarded.
|
|
4498
|
+
Pass `response_headers_type` to get the response headers parsed into `.typed_headers`.
|
|
4499
|
+
|
|
4500
|
+
`timeout` overrides the client's own for this call only; `skip_auth` omits the
|
|
4501
|
+
`Authorization` header (and skips invoking `bearer_auth`) for this call; `raises` overrides
|
|
4502
|
+
the client's own `raises` (the `HTTPResponseError` subclass a 4xx/5xx is raised as) for
|
|
4503
|
+
this call.
|
|
4504
|
+
"""
|
|
4505
|
+
_validate_raises(raises)
|
|
4506
|
+
request_builder = await self._prepare_request(
|
|
4507
|
+
self._client.request("OPTIONS", path), params, headers, timeout, skip_auth=skip_auth
|
|
4508
|
+
)
|
|
4509
|
+
started = time.monotonic()
|
|
4510
|
+
raw_response, request_info = await _send(request_builder)
|
|
4511
|
+
elapsed = time.monotonic() - started
|
|
4512
|
+
if error_for_status:
|
|
4513
|
+
await self._check_status(raw_response, raises, request_info)
|
|
4514
|
+
response_headers = _LazyHeaders(raw_response.headers)
|
|
4515
|
+
typed_headers = (
|
|
4516
|
+
None
|
|
4517
|
+
if response_headers_type is None
|
|
4518
|
+
else _parse_typed_headers(response_headers, response_headers_type)
|
|
4519
|
+
)
|
|
4520
|
+
return Response(
|
|
4521
|
+
None,
|
|
4522
|
+
raw_response.status,
|
|
4523
|
+
response_headers,
|
|
4524
|
+
typed_headers,
|
|
4525
|
+
request_info,
|
|
4526
|
+
raw_response.version,
|
|
4527
|
+
elapsed,
|
|
4528
|
+
)
|
|
4529
|
+
|
|
4453
4530
|
|
|
4454
4531
|
class SyncHTTPClient:
|
|
4455
4532
|
"""Sync typed HTTP client: `with SyncHTTPClient(base_url=...) as client:`.
|
|
@@ -4457,7 +4534,13 @@ class SyncHTTPClient:
|
|
|
4457
4534
|
See `HTTPClient` for the async mirror — same methods, same overload shapes.
|
|
4458
4535
|
"""
|
|
4459
4536
|
|
|
4460
|
-
_default_retry_methods: ClassVar[frozenset[str]] = frozenset({
|
|
4537
|
+
_default_retry_methods: ClassVar[frozenset[str]] = frozenset({
|
|
4538
|
+
"GET",
|
|
4539
|
+
"PUT",
|
|
4540
|
+
"DELETE",
|
|
4541
|
+
"HEAD",
|
|
4542
|
+
"OPTIONS",
|
|
4543
|
+
})
|
|
4461
4544
|
# The streaming verbs (`sse`, `stream_get`, `stream_post`, `download`) swap the client's total
|
|
4462
4545
|
# `timeout` for this per-request one whenever the caller doesn't pass their own: pyreqwest has
|
|
4463
4546
|
# no "no timeout" override, and a total timeout on a long-lived body kills every healthy
|
|
@@ -4525,12 +4608,13 @@ class SyncHTTPClient:
|
|
|
4525
4608
|
`bearer_token` is a static token; `bearer_auth` is a callable resolved fresh on every
|
|
4526
4609
|
request; `basic_auth` is a `(username, password)` pair — provide at most one of the
|
|
4527
4610
|
three. `max_retries` enables a real retry middleware (backoff, `Retry-After`-aware); with
|
|
4528
|
-
no `retry_methods`, only the idempotent verbs (`GET`/`PUT`/`DELETE`/`HEAD`)
|
|
4529
|
-
`backoff_base` scales the wait between attempts (`backoff_base * 2 **
|
|
4530
|
-
0.1 default waits 0.1s then 0.2s) — a `Retry-After` header on the
|
|
4531
|
-
computed delay, up to `max_retry_after` (60s by default): a server
|
|
4532
|
-
ends retrying, and its 429/503 is raised as `HTTPResponseError`
|
|
4533
|
-
from `e.headers["Retry-After"]`. Pass `max_retry_after=None`
|
|
4611
|
+
no `retry_methods`, only the idempotent verbs (`GET`/`PUT`/`DELETE`/`HEAD`/`OPTIONS`)
|
|
4612
|
+
retry, and `backoff_base` scales the wait between attempts (`backoff_base * 2 **
|
|
4613
|
+
attempt`, so the 0.1 default waits 0.1s then 0.2s) — a `Retry-After` header on the
|
|
4614
|
+
response wins over the computed delay, up to `max_retry_after` (60s by default): a server
|
|
4615
|
+
asking for a longer wait ends retrying, and its 429/503 is raised as `HTTPResponseError`
|
|
4616
|
+
so you can schedule a retry from `e.headers["Retry-After"]`. Pass `max_retry_after=None`
|
|
4617
|
+
to honour any wait.
|
|
4534
4618
|
|
|
4535
4619
|
`connect_timeout` bounds only the TCP connect phase (separate from `timeout`, which
|
|
4536
4620
|
covers the whole request); `read_timeout` bounds the idle gap between two consecutive
|
|
@@ -6390,3 +6474,73 @@ class SyncHTTPClient:
|
|
|
6390
6474
|
raw_response.version,
|
|
6391
6475
|
elapsed,
|
|
6392
6476
|
)
|
|
6477
|
+
|
|
6478
|
+
@overload
|
|
6479
|
+
def options(
|
|
6480
|
+
self,
|
|
6481
|
+
path: str,
|
|
6482
|
+
*,
|
|
6483
|
+
params: Params | None = None,
|
|
6484
|
+
headers: Headers | None = None,
|
|
6485
|
+
timeout: float | None = None,
|
|
6486
|
+
skip_auth: bool = False,
|
|
6487
|
+
error_for_status: bool = True,
|
|
6488
|
+
raises: type[HTTPResponseError[Any]] | None = None,
|
|
6489
|
+
) -> Response[None]: ...
|
|
6490
|
+
@overload
|
|
6491
|
+
def options[THeaders: TypedHeaders](
|
|
6492
|
+
self,
|
|
6493
|
+
path: str,
|
|
6494
|
+
*,
|
|
6495
|
+
params: Params | None = None,
|
|
6496
|
+
headers: Headers | None = None,
|
|
6497
|
+
timeout: float | None = None,
|
|
6498
|
+
skip_auth: bool = False,
|
|
6499
|
+
response_headers_type: type[THeaders],
|
|
6500
|
+
error_for_status: bool = True,
|
|
6501
|
+
raises: type[HTTPResponseError[Any]] | None = None,
|
|
6502
|
+
) -> Response[None, THeaders]: ...
|
|
6503
|
+
def options(
|
|
6504
|
+
self,
|
|
6505
|
+
path: str,
|
|
6506
|
+
*,
|
|
6507
|
+
params: Params | None = None,
|
|
6508
|
+
headers: Headers | None = None,
|
|
6509
|
+
timeout: float | None = None,
|
|
6510
|
+
skip_auth: bool = False,
|
|
6511
|
+
response_headers_type: type[TypedHeaders] | None = None,
|
|
6512
|
+
error_for_status: bool = True,
|
|
6513
|
+
raises: type[HTTPResponseError[Any]] | None = None,
|
|
6514
|
+
) -> Response[None, Any]:
|
|
6515
|
+
"""OPTIONS `path` (e.g. a CORS preflight) — status and headers only, any body is discarded.
|
|
6516
|
+
Pass `response_headers_type` to get the response headers parsed into `.typed_headers`.
|
|
6517
|
+
|
|
6518
|
+
`timeout` overrides the client's own for this call only; `skip_auth` omits the
|
|
6519
|
+
`Authorization` header (and skips invoking `bearer_auth`) for this call; `raises` overrides
|
|
6520
|
+
the client's own `raises` (the `HTTPResponseError` subclass a 4xx/5xx is raised as) for
|
|
6521
|
+
this call.
|
|
6522
|
+
"""
|
|
6523
|
+
_validate_raises(raises)
|
|
6524
|
+
request_builder = self._prepare_request(
|
|
6525
|
+
self._client.request("OPTIONS", path), params, headers, timeout, skip_auth=skip_auth
|
|
6526
|
+
)
|
|
6527
|
+
started = time.monotonic()
|
|
6528
|
+
raw_response, request_info = _send_sync(request_builder)
|
|
6529
|
+
elapsed = time.monotonic() - started
|
|
6530
|
+
if error_for_status:
|
|
6531
|
+
self._check_status(raw_response, raises, request_info)
|
|
6532
|
+
response_headers = _LazyHeaders(raw_response.headers)
|
|
6533
|
+
typed_headers = (
|
|
6534
|
+
None
|
|
6535
|
+
if response_headers_type is None
|
|
6536
|
+
else _parse_typed_headers(response_headers, response_headers_type)
|
|
6537
|
+
)
|
|
6538
|
+
return Response(
|
|
6539
|
+
None,
|
|
6540
|
+
raw_response.status,
|
|
6541
|
+
response_headers,
|
|
6542
|
+
typed_headers,
|
|
6543
|
+
request_info,
|
|
6544
|
+
raw_response.version,
|
|
6545
|
+
elapsed,
|
|
6546
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|