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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: lothc
3
- Version: 0.2.1
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`/`head`'s result and to
171
- `HTTPResponseError.request` — the target you asked for, not necessarily the one a final
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({"GET", "PUT", "DELETE", "HEAD"})
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`) retry, and
2633
- `backoff_base` scales the wait between attempts (`backoff_base * 2 ** attempt`, so the
2634
- 0.1 default waits 0.1s then 0.2s) — a `Retry-After` header on the response wins over the
2635
- computed delay, up to `max_retry_after` (60s by default): a server asking for a longer wait
2636
- ends retrying, and its 429/503 is raised as `HTTPResponseError` so you can schedule a retry
2637
- from `e.headers["Retry-After"]`. Pass `max_retry_after=None` to honour any wait.
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({"GET", "PUT", "DELETE", "HEAD"})
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`) retry, and
4529
- `backoff_base` scales the wait between attempts (`backoff_base * 2 ** attempt`, so the
4530
- 0.1 default waits 0.1s then 0.2s) — a `Retry-After` header on the response wins over the
4531
- computed delay, up to `max_retry_after` (60s by default): a server asking for a longer wait
4532
- ends retrying, and its 429/503 is raised as `HTTPResponseError` so you can schedule a retry
4533
- from `e.headers["Retry-After"]`. Pass `max_retry_after=None` to honour any wait.
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