Skip to content

Commit fec9d9e

Browse files
committed
Bump
1 parent 895d856 commit fec9d9e

18 files changed

Lines changed: 144 additions & 115 deletions

lithic/_base_client.py

Lines changed: 50 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,8 @@
2626
import pydantic
2727
from pydantic import PrivateAttr
2828

29-
from ._types import (
30-
Query,
31-
ModelT,
32-
Timeout,
33-
Transport,
34-
ProxiesTypes,
35-
RequestOptions,
36-
FinalRequestOptions,
37-
)
38-
from ._models import BaseModel, NoneModel, GenericModel
29+
from ._types import Query, ModelT, Timeout, Transport, ProxiesTypes, RequestOptions
30+
from ._models import BaseModel, NoneModel, GenericModel, FinalRequestOptions
3931
from .exceptions import (
4032
APITimeoutError,
4133
APIConnectionError,
@@ -65,14 +57,9 @@ class BasePage(GenericModel, Generic[ModelT, PageParamsT]):
6557
_model: Type[ModelT] = PrivateAttr()
6658

6759
def has_next_page(self) -> bool:
68-
...
60+
return self.next_page_params() is not None
6961

7062
def next_page_params(self) -> Optional[PageParamsT]:
71-
if self.has_next_page():
72-
return self._next_page_params()
73-
return None
74-
75-
def _next_page_params(self) -> PageParamsT:
7663
...
7764

7865
def _get_page_items(self) -> Iterable[ModelT]:
@@ -115,15 +102,14 @@ def iter_pages(self: SyncPageT) -> Iterator[SyncPageT]:
115102
return
116103

117104
def get_next_page(self: SyncPageT) -> SyncPageT:
118-
page_params = self._next_page_params()
119-
params = self._options.get("params", {}) or {}
120-
options: Mapping[str, object] = {**self._options, "params": {**params, **page_params}}
121-
return self._client.request_api_list(
122-
self._model,
123-
page=self.__class__,
124-
# TODO: validate that what we pass is actually valid at runtime
125-
options=options, # type: ignore
126-
)
105+
next_params = self.next_page_params()
106+
if not next_params:
107+
raise StopIteration()
108+
109+
options = self._options.copy()
110+
options.params = {**options.params, **next_params}
111+
112+
return self._client.request_api_list(self._model, page=self.__class__, options=options)
127113

128114

129115
class AsyncPaginator(Generic[ModelT, AsyncPageT]):
@@ -189,15 +175,13 @@ async def iter_pages(self: AsyncPageT) -> AsyncIterator[AsyncPageT]:
189175
return
190176

191177
async def get_next_page(self: AsyncPageT) -> AsyncPageT:
192-
page_params = self._next_page_params()
193-
params = self._options.get("params", {}) or {}
194-
options: Mapping[str, object] = {**self._options, "params": {**params, **page_params}}
195-
return await self._client.request_api_list(
196-
self._model,
197-
page=self.__class__,
198-
# TODO: validate that what we pass is actually valid at runtime
199-
options=options, # type: ignore
200-
)
178+
next_params = self.next_page_params()
179+
if not next_params:
180+
raise StopIteration()
181+
182+
options = self._options.copy()
183+
options.params = {**options.params, **next_params}
184+
return await self._client.request_api_list(self._model, page=self.__class__, options=options)
201185

202186

203187
class BaseClient:
@@ -226,18 +210,13 @@ def remaining_retries(
226210
remaining_retries: Optional[int],
227211
options: FinalRequestOptions,
228212
) -> int:
229-
return remaining_retries if remaining_retries is not None else options.get("max_retries", self.max_retries)
213+
return remaining_retries if remaining_retries is not None else options.get_max_retries(self.max_retries)
230214

231215
def prepare_request_args(
232216
self,
233217
options: FinalRequestOptions,
234218
) -> Dict[str, Any]:
235-
headers = {**self.default_headers(), **options.get("headers", {})}
236-
req_args: Dict[str, Any] = {**options, "headers": headers, "timeout": options.get("timeout", self.timeout)}
237-
238-
# ensure we only returned expected keyword arguments to `build_request()` to avoid a `TypeError: build_request()
239-
# got an unexpected keyword argument 'max_retries'`.
240-
return {k: v for k, v in req_args.items() if k in [p.name for p in BUILD_REQUEST_PARAMS]}
219+
return options.to_request_args(default_headers=self.default_headers(), default_timeout=self.timeout)
241220

242221
def process_response(
243222
self, model: Type[ResponseT], options: FinalRequestOptions, response: httpx.Response
@@ -282,7 +261,7 @@ def calculate_retry_timeout(
282261
options: FinalRequestOptions,
283262
response_headers: Optional[httpx.Headers] = None,
284263
) -> float:
285-
max_retries = options.get("max_retries", self.max_retries)
264+
max_retries = options.get_max_retries(self.max_retries)
286265
try:
287266
# About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
288267
#
@@ -416,7 +395,7 @@ def request_api_list(
416395
self,
417396
model: Type[ModelT],
418397
page: Type[SyncPageT],
419-
options: FinalRequestOptions = {},
398+
options: FinalRequestOptions,
420399
) -> SyncPageT:
421400
resp = self.request(page, options)
422401
resp._set_private_attributes( # pyright: ignore[reportPrivateUsage]
@@ -431,10 +410,10 @@ def get(
431410
path: str,
432411
*,
433412
model: Type[ResponseT],
434-
query: Query | None = None,
413+
query: Query = {},
435414
options: RequestOptions = {},
436415
) -> ResponseT:
437-
opts = FinalRequestOptions(method="get", url=path, params=query, **options) # type: ignore[misc]
416+
opts = FinalRequestOptions(method="get", url=path, params=query, **options)
438417
return self.request(model, opts)
439418

440419
def post(
@@ -445,7 +424,7 @@ def post(
445424
body: Query | None = None,
446425
options: RequestOptions = {},
447426
) -> ResponseT:
448-
opts = FinalRequestOptions(method="post", url=path, json=body, **options) # type: ignore[misc]
427+
opts = FinalRequestOptions(method="post", url=path, json_data=body, **options)
449428
return self.request(model, opts)
450429

451430
def patch(
@@ -456,7 +435,7 @@ def patch(
456435
body: Query | None = None,
457436
options: RequestOptions = {},
458437
) -> ResponseT:
459-
opts = FinalRequestOptions(method="patch", url=path, json=body, **options) # type: ignore[misc]
438+
opts = FinalRequestOptions(method="patch", url=path, json_data=body, **options)
460439
return self.request(model, opts)
461440

462441
def put(
@@ -467,7 +446,7 @@ def put(
467446
body: Query | None = None,
468447
options: RequestOptions = {},
469448
) -> ResponseT:
470-
opts = FinalRequestOptions(method="put", url=path, json=body, **options) # type: ignore[misc]
449+
opts = FinalRequestOptions(method="put", url=path, json_data=body, **options)
471450
return self.request(model, opts)
472451

473452
def delete(
@@ -478,19 +457,19 @@ def delete(
478457
body: Query | None = None,
479458
options: RequestOptions = {},
480459
) -> ResponseT:
481-
opts = FinalRequestOptions(method="delete", url=path, json=body, **options) # type: ignore[misc]
460+
opts = FinalRequestOptions(method="delete", url=path, json_data=body, **options)
482461
return self.request(model, opts)
483462

484463
def get_api_list(
485464
self,
486465
path: str,
487466
*,
488-
query: Query | None = None,
489467
model: Type[ModelT],
490468
page: Type[SyncPageT],
469+
query: Query = {},
491470
options: RequestOptions = {},
492471
) -> SyncPageT:
493-
opts = FinalRequestOptions(method="get", url=path, params=query, **options) # type: ignore[misc]
472+
opts = FinalRequestOptions(method="get", url=path, params=query, **options)
494473
return self.request_api_list(model, page, opts)
495474

496475

@@ -584,7 +563,7 @@ def request_api_list(
584563
self,
585564
model: Type[ModelT],
586565
page: Type[AsyncPageT],
587-
options: FinalRequestOptions = {},
566+
options: FinalRequestOptions,
588567
) -> AsyncPaginator[ModelT, AsyncPageT]:
589568
return AsyncPaginator(client=self, options=options, page_cls=page, model=model)
590569

@@ -593,10 +572,10 @@ async def get(
593572
path: str,
594573
*,
595574
model: Type[ResponseT],
596-
query: Query | None = None,
575+
query: Query = {},
597576
options: RequestOptions = {},
598577
) -> ResponseT:
599-
opts = FinalRequestOptions(method="get", url=path, params=query, **options) # type: ignore[misc]
578+
opts = FinalRequestOptions(method="get", url=path, params=query, **options)
600579
return await self.request(model, opts)
601580

602581
async def post(
@@ -607,7 +586,7 @@ async def post(
607586
body: Query | None = None,
608587
options: RequestOptions = {},
609588
) -> ResponseT:
610-
opts = FinalRequestOptions(method="post", url=path, json=body, **options) # type: ignore[misc]
589+
opts = FinalRequestOptions(method="post", url=path, json_data=body, **options)
611590
return await self.request(model, opts)
612591

613592
async def patch(
@@ -618,7 +597,7 @@ async def patch(
618597
body: Query | None = None,
619598
options: RequestOptions = {},
620599
) -> ResponseT:
621-
opts = FinalRequestOptions(method="patch", url=path, json=body, **options) # type: ignore[misc]
600+
opts = FinalRequestOptions(method="patch", url=path, json_data=body, **options)
622601
return await self.request(model, opts)
623602

624603
async def put(
@@ -629,7 +608,7 @@ async def put(
629608
body: Query | None = None,
630609
options: RequestOptions = {},
631610
) -> ResponseT:
632-
opts = FinalRequestOptions(method="put", url=path, json=body, **options) # type: ignore[misc]
611+
opts = FinalRequestOptions(method="put", url=path, json_data=body, **options)
633612
return await self.request(model, opts)
634613

635614
async def delete(
@@ -640,19 +619,19 @@ async def delete(
640619
body: Query | None = None,
641620
options: RequestOptions = {},
642621
) -> ResponseT:
643-
opts = FinalRequestOptions(method="delete", url=path, json=body, **options) # type: ignore[misc]
622+
opts = FinalRequestOptions(method="delete", url=path, json_data=body, **options)
644623
return await self.request(model, opts)
645624

646625
def get_api_list(
647626
self,
648627
path: str,
649628
*,
650-
query: Query | None = None,
651629
model: Type[ModelT],
652630
page: Type[AsyncPageT],
631+
query: Query = {},
653632
options: RequestOptions = {},
654633
) -> AsyncPaginator[ModelT, AsyncPageT]:
655-
opts = FinalRequestOptions(method="get", url=path, params=query, **options) # type: ignore[misc]
634+
opts = FinalRequestOptions(method="get", url=path, params=query, **options)
656635
return self.request_api_list(model, page, opts)
657636

658637

@@ -662,10 +641,14 @@ def make_request_options(
662641
timeout: float | Timeout | None = None,
663642
) -> RequestOptions:
664643
"""Create a dict of type RequestOptions without keys of None values."""
665-
d = {
666-
"headers": headers,
667-
"max_retries": max_retries,
668-
"timeout": timeout,
669-
}
670-
filtered = {k: v for k, v in d.items() if v is not None}
671-
return RequestOptions(**filtered) # type: ignore
644+
options: RequestOptions = {}
645+
if headers is not None:
646+
options["headers"] = headers
647+
648+
if max_retries is not None:
649+
options["max_retries"] = max_retries
650+
651+
if timeout is not None:
652+
options["timeout"] = timeout
653+
654+
return options

lithic/_client.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from . import resources
99
from ._types import Timeout, Transport, ProxiesTypes, RequestOptions
1010
from ._version import __version__
11-
from .pagination import SyncPage, AsyncPage
1211
from ._base_client import (
1312
DEFAULT_TIMEOUT,
1413
DEFAULT_MAX_RETRIES,

lithic/_models.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
from __future__ import annotations
22

3-
from typing import Any, Type, cast
3+
from typing import Any, Dict, Type, Union, Mapping, cast
44

55
import pydantic
66
import pydantic.generics
77
from pydantic.typing import is_literal_type
88

9-
from ._types import ModelT
9+
from ._types import ModelT, Timeout, NotGiven
1010

1111
__all__ = ["BaseModel", "GenericModel", "StringModel", "NoneModel"]
1212

@@ -62,3 +62,39 @@ class StringModel(BaseModel):
6262

6363
class NoneModel(BaseModel):
6464
pass
65+
66+
67+
class FinalRequestOptions(BaseModel):
68+
method: str
69+
url: str
70+
params: Mapping[str, object] = {}
71+
headers: Union[Dict[str, str], NotGiven] = NotGiven()
72+
max_retries: Union[int, NotGiven] = NotGiven()
73+
timeout: Union[float, Timeout, None, NotGiven] = NotGiven()
74+
75+
# It should be noted that we cannot use `json` here as that would override
76+
# a BaseModel method in an incompatible fashion.
77+
json_data: Union[object, None] = None
78+
79+
class Config(pydantic.BaseConfig):
80+
arbitrary_types_allowed: bool = True
81+
82+
def get_max_retries(self, max_retries: int) -> int:
83+
if isinstance(self.max_retries, NotGiven):
84+
return max_retries
85+
return self.max_retries
86+
87+
def to_request_args(
88+
self, default_headers: Dict[str, str], default_timeout: Union[float, Timeout, None]
89+
) -> Dict[str, object]:
90+
return {
91+
"headers": {
92+
**default_headers,
93+
**({} if isinstance(self.headers, NotGiven) else self.headers),
94+
},
95+
"timeout": default_timeout if isinstance(self.timeout, NotGiven) else self.timeout,
96+
"method": self.method,
97+
"url": self.url,
98+
"params": self.params,
99+
"json": self.json_data,
100+
}

lithic/_types.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Dict, Union, Mapping, TypeVar
1+
from typing import Dict, Union, Mapping, TypeVar
22

33
import pydantic
44
from httpx import Proxy, Timeout, BaseTransport
@@ -18,11 +18,21 @@ class RequestOptions(TypedDict, total=False):
1818
timeout: Union[float, Timeout]
1919

2020

21-
class FinalRequestOptions(TypedDict, total=False):
22-
method: str
23-
url: str
24-
json: Any
25-
params: Any
26-
headers: Dict[str, str]
27-
max_retries: int
28-
timeout: Union[float, Timeout, None]
21+
# Sentinel class used until PEP 0661 is accepted
22+
class NotGiven:
23+
"""For certain parameters such as `timeout=...` the intent can be made more clear
24+
by typing the parameter with this class rather than using None, for example:
25+
26+
```py
27+
def get(timeout: Union[int, NotGiven] = NotGiven()) -> Response: ...
28+
```
29+
30+
relays the intention more clearly than:
31+
32+
```py
33+
def get(timeout: Optional[int] = None) -> Response: ...
34+
```
35+
36+
This solution also allows you to indicate an "unset" state that is uniquely distinct
37+
from `None` which means you can explicitly pass `None` to disable the timeout entirely.
38+
"""

0 commit comments

Comments
 (0)