diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..4c0a675e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: CI +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + lint: + name: lint + runs-on: ubuntu-latest + if: github.repository == 'Finch-API/finch-api-python' + + steps: + - uses: actions/checkout@v3 + + - name: Install Rye + run: | + curl -sSf https://rye-up.com/get | bash + echo "$HOME/.rye/shims" >> $GITHUB_PATH + env: + RYE_VERSION: 0.15.2 + RYE_INSTALL_OPTION: "--yes" + + - name: Install dependencies + run: | + rye sync --all-features + + - name: Run ruff + run: | + rye run check:ruff + + - name: Run type checking + run: | + rye run typecheck + + - name: Ensure importable + run: | + rye run python -c 'import finch' diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..43077b24 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.9.18 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ccdf8aa7..6b7b74c5 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.2.3" + ".": "0.3.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 574e0dbf..be91b560 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 0.3.0 (2023-10-27) + +Full Changelog: [v0.2.3...v0.3.0](https://github.com/finch-api/finch-api-python/compare/v0.2.3...v0.3.0) + +### Features + +* **client:** adjust retry behavior to be exponential backoff ([#149](https://github.com/finch-api/finch-api-python/issues/149)) ([6c76643](https://github.com/finch-api/finch-api-python/commit/6c766434ba25b5684c41261dfd68355ea9c347ad)) +* **client:** improve file upload types ([#148](https://github.com/finch-api/finch-api-python/issues/148)) ([7f9db48](https://github.com/finch-api/finch-api-python/commit/7f9db48ced6c9914cc65ad6e071f3e10ec02885c)) +* **client:** support accessing raw response objects ([#154](https://github.com/finch-api/finch-api-python/issues/154)) ([10638eb](https://github.com/finch-api/finch-api-python/commit/10638eb2689ce1a4a522c989df8a9a474e0590f8)) + + +### Chores + +* **internal:** require explicit overrides ([#153](https://github.com/finch-api/finch-api-python/issues/153)) ([9ffdf66](https://github.com/finch-api/finch-api-python/commit/9ffdf669051d12e34078205d49d12b7c55909611)) + + +### Documentation + +* improve to dictionary example ([#151](https://github.com/finch-api/finch-api-python/issues/151)) ([098d453](https://github.com/finch-api/finch-api-python/commit/098d453e1f1170d14362090500c460fba36d70e9)) + ## 0.2.3 (2023-10-20) Full Changelog: [v0.2.2...v0.2.3](https://github.com/Finch-API/finch-api-python/compare/v0.2.2...v0.2.3) diff --git a/README.md b/README.md index 12a04b7f..43877d82 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Functionality between the synchronous and asynchronous clients is otherwise iden ## Using types -Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev), which provide helper methods for things like serializing back into JSON ([v1](https://docs.pydantic.dev/1.10/usage/models/), [v2](https://docs.pydantic.dev/latest/usage/serialization/)). To get a dictionary, call `dict(model)`. +Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev), which provide helper methods for things like serializing back into JSON ([v1](https://docs.pydantic.dev/1.10/usage/models/), [v2](https://docs.pydantic.dev/latest/usage/serialization/)). To get a dictionary, call `model.model_dump()`. Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. @@ -296,6 +296,25 @@ if response.my_field is None: print('Got json like {"my_field": null}.') ``` +### Accessing raw response data (e.g. headers) + +The "raw" Response object can be accessed by prefixing `.with_raw_response.` to any HTTP method call. + +```py +from finch import Finch + +client = Finch() +page = client.hris.directory.with_raw_response.list() +response = page.individuals[0] + +print(response.headers.get('X-My-Header')) + +directory = response.parse() # get the object that `hris.directory.list()` would have returned +print(directory.first_name) +``` + +These methods return an [`APIResponse`](https://github.com/Finch-API/finch-api-python/src/finch/_response.py) object. + ### Configuring the HTTP client You can directly override the [httpx client](https://www.python-httpx.org/api/#client) to customize it for your use case, including: diff --git a/mypy.ini b/mypy.ini index 8ea2d5af..116e626a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,7 +1,11 @@ [mypy] pretty = True show_error_codes = True -exclude = _dev + +# Exclude _files.py because mypy isn't smart enough to apply +# the correct type narrowing and as this is an internal module +# it's fine to just use Pyright. +exclude = ^(src/finch/_files\.py|_dev/.*\.py)$ strict_equality = True implicit_reexport = True diff --git a/pyproject.toml b/pyproject.toml index 4f84ce66..c6bcf833 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "finch-api" -version = "0.2.3" +version = "0.3.0" description = "Client library for the Finch API" readme = "README.md" license = "Apache-2.0" @@ -38,6 +38,7 @@ dev-dependencies = [ "isort==5.10.1", "time-machine==2.9.0", "nox==2023.4.22", + "dirty-equals>=0.6.0", ] @@ -53,6 +54,16 @@ format = { chain = [ "format:ruff" = "ruff --fix ." "format:isort" = "isort ." +"check:ruff" = "ruff ." + +typecheck = { chain = [ + "typecheck:pyright", + "typecheck:mypy" +]} +"typecheck:pyright" = "pyright" +"typecheck:verify-types" = "pyright --verifytypes finch --ignoreexternal" +"typecheck:mypy" = "mypy --enable-incomplete-feature=Unpack ." + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" @@ -90,6 +101,9 @@ exclude = [ ".venv", ".nox", ] + +reportImplicitOverride = true + reportImportCycles = false reportPrivateUsage = false diff --git a/requirements-dev.lock b/requirements-dev.lock index 505b6ccf..8d5e2cfd 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -15,6 +15,7 @@ black==23.3.0 certifi==2023.7.22 click==8.1.7 colorlog==6.7.0 +dirty-equals==0.6.0 distlib==0.3.7 distro==1.8.0 exceptiongroup==1.1.3 diff --git a/src/finch/_base_client.py b/src/finch/_base_client.py index c49cc042..6e4be17a 100644 --- a/src/finch/_base_client.py +++ b/src/finch/_base_client.py @@ -29,7 +29,7 @@ overload, ) from functools import lru_cache -from typing_extensions import Literal, get_args, get_origin +from typing_extensions import Literal, override import anyio import httpx @@ -40,6 +40,7 @@ from . import _exceptions from ._qs import Querystring +from ._files import to_httpx_files, async_to_httpx_files from ._types import ( NOT_GIVEN, Body, @@ -48,11 +49,11 @@ ModelT, Headers, Timeout, - NoneType, NotGiven, ResponseT, Transport, AnyMapping, + PostParser, ProxiesTypes, RequestFiles, AsyncTransport, @@ -62,20 +63,16 @@ ) from ._utils import is_dict, is_given, is_mapping from ._compat import model_copy, model_dump -from ._models import ( - BaseModel, - GenericModel, - FinalRequestOptions, - validate_type, - construct_type, +from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type +from ._response import APIResponse +from ._constants import ( + DEFAULT_LIMITS, + DEFAULT_TIMEOUT, + DEFAULT_MAX_RETRIES, + RAW_RESPONSE_HEADER, ) from ._streaming import Stream, AsyncStream -from ._exceptions import ( - APIStatusError, - APITimeoutError, - APIConnectionError, - APIResponseValidationError, -) +from ._exceptions import APIStatusError, APITimeoutError, APIConnectionError log: logging.Logger = logging.getLogger(__name__) @@ -100,19 +97,6 @@ HTTPX_DEFAULT_TIMEOUT = Timeout(5.0) -# default timeout is 1 minute -DEFAULT_TIMEOUT = Timeout(timeout=60.0, connect=5.0) -DEFAULT_MAX_RETRIES = 2 -DEFAULT_LIMITS = Limits(max_connections=100, max_keepalive_connections=20) - - -class MissingStreamClassError(TypeError): - def __init__(self) -> None: - super().__init__( - "The `stream` argument was set to `True` but the `stream_cls` argument was not given. See `finch._streaming` for reference", - ) - - class PageInfo: """Stores the necesary information to build the request to retrieve the next page. @@ -157,7 +141,7 @@ class BasePage(GenericModel, Generic[ModelT]): Methods: has_next_page(): Check if there is another page available - next_page_info(): Get the necesary information to make a request for the next page + next_page_info(): Get the necessary information to make a request for the next page """ _options: FinalRequestOptions = PrivateAttr() @@ -181,6 +165,7 @@ def _params_from_url(self, url: URL) -> httpx.QueryParams: def _info_to_options(self, info: PageInfo) -> FinalRequestOptions: options = model_copy(self._options) + options._strip_raw_response_header() if not isinstance(info.params, NotGiven): options.params = {**options.params, **info.params} @@ -259,13 +244,17 @@ def __await__(self) -> Generator[Any, None, AsyncPageT]: return self._get_page().__await__() async def _get_page(self) -> AsyncPageT: - page = await self._client.request(self._page_cls, self._options) - page._set_private_attributes( # pyright: ignore[reportPrivateUsage] - model=self._model, - options=self._options, - client=self._client, - ) - return page + def _parser(resp: AsyncPageT) -> AsyncPageT: + resp._set_private_attributes( + model=self._model, + options=self._options, + client=self._client, + ) + return resp + + self._options.post_parser = _parser + + return await self._client.request(self._page_cls, self._options) async def __aiter__(self) -> AsyncIterator[ModelT]: # https://github.com/microsoft/pyright/issues/3464 @@ -316,9 +305,10 @@ async def get_next_page(self: AsyncPageT) -> AsyncPageT: _HttpxClientT = TypeVar("_HttpxClientT", bound=Union[httpx.Client, httpx.AsyncClient]) +_DefaultStreamT = TypeVar("_DefaultStreamT", bound=Union[Stream[Any], AsyncStream[Any]]) -class BaseClient(Generic[_HttpxClientT]): +class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]): _client: _HttpxClientT _version: str _base_url: URL @@ -329,6 +319,7 @@ class BaseClient(Generic[_HttpxClientT]): _transport: Transport | AsyncTransport | None _strict_response_validation: bool _idempotency_header: str | None + _default_stream_cls: type[_DefaultStreamT] | None = None def __init__( self, @@ -503,80 +494,28 @@ def _serialize_multipartform(self, data: Mapping[object, object]) -> dict[str, o serialized[key] = value return serialized - def _extract_stream_chunk_type(self, stream_cls: type) -> type: - args = get_args(stream_cls) - if not args: - raise TypeError( - f"Expected stream_cls to have been given a generic type argument, e.g. Stream[Foo] but received {stream_cls}", - ) - return cast(type, args[0]) - def _process_response( self, *, cast_to: Type[ResponseT], - options: FinalRequestOptions, # noqa: ARG002 + options: FinalRequestOptions, response: httpx.Response, + stream: bool, + stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, ) -> ResponseT: - if cast_to is NoneType: - return cast(ResponseT, None) - - if cast_to == str: - return cast(ResponseT, response.text) - - origin = get_origin(cast_to) or cast_to - - if inspect.isclass(origin) and issubclass(origin, httpx.Response): - # Because of the invariance of our ResponseT TypeVar, users can subclass httpx.Response - # and pass that class to our request functions. We cannot change the variance to be either - # covariant or contravariant as that makes our usage of ResponseT illegal. We could construct - # the response class ourselves but that is something that should be supported directly in httpx - # as it would be easy to incorrectly construct the Response object due to the multitude of arguments. - if cast_to != httpx.Response: - raise ValueError(f"Subclasses of httpx.Response cannot be passed to `cast_to`") - return cast(ResponseT, response) - - # The check here is necessary as we are subverting the the type system - # with casts as the relationship between TypeVars and Types are very strict - # which means we must return *exactly* what was input or transform it in a - # way that retains the TypeVar state. As we cannot do that in this function - # then we have to resort to using `cast`. At the time of writing, we know this - # to be safe as we have handled all the types that could be bound to the - # `ResponseT` TypeVar, however if that TypeVar is ever updated in the future, then - # this function would become unsafe but a type checker would not report an error. - if ( - cast_to is not UnknownResponse - and not origin is list - and not origin is dict - and not origin is Union - and not issubclass(origin, BaseModel) - ): - raise RuntimeError( - f"Invalid state, expected {cast_to} to be a subclass type of {BaseModel}, {dict}, {list} or {Union}." - ) - - # split is required to handle cases where additional information is included - # in the response, e.g. application/json; charset=utf-8 - content_type, *_ = response.headers.get("content-type").split(";") - if content_type != "application/json": - if self._strict_response_validation: - raise APIResponseValidationError( - response=response, - message=f"Expected Content-Type response header to be `application/json` but received `{content_type}` instead.", - body=response.text, - ) - - # If the API responds with content that isn't JSON then we just return - # the (decoded) text without performing any parsing so that you can still - # handle the response however you need to. - return response.text # type: ignore + api_response = APIResponse( + raw=response, + client=self, + cast_to=cast_to, + stream=stream, + stream_cls=stream_cls, + options=options, + ) - data = response.json() + if response.request.headers.get(RAW_RESPONSE_HEADER) == "true": + return cast(ResponseT, api_response) - try: - return self._process_response_data(data=data, cast_to=cast_to, response=response) - except pydantic.ValidationError as err: - raise APIResponseValidationError(response=response, body=data) from err + return api_response.parse() def _process_response_data( self, @@ -690,15 +629,15 @@ def _calculate_retry_timeout( return retry_after initial_retry_delay = 0.5 - max_retry_delay = 2.0 + max_retry_delay = 8.0 nb_retries = max_retries - remaining_retries # Apply exponential backoff, but not more than the max. - sleep_seconds = min(initial_retry_delay * pow(nb_retries - 1, 2), max_retry_delay) + sleep_seconds = min(initial_retry_delay * pow(2.0, nb_retries), max_retry_delay) # Apply some jitter, plus-or-minus half a second. - jitter = random() - 0.5 - timeout = sleep_seconds + jitter + jitter = 1 - 0.25 * random() + timeout = sleep_seconds * jitter return timeout if timeout >= 0 else 0 def _should_retry(self, response: httpx.Response) -> bool: @@ -733,7 +672,7 @@ def _idempotency_key(self) -> str: return f"stainless-python-retry-{uuid.uuid4()}" -class SyncAPIClient(BaseClient[httpx.Client]): +class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]): _client: httpx.Client _has_custom_http_client: bool _default_stream_cls: type[Stream[Any]] | None = None @@ -929,23 +868,32 @@ def _request( raise self._make_status_error_from_response(err.response) from None except httpx.TimeoutException as err: if retries > 0: - return self._retry_request(options, cast_to, retries, stream=stream, stream_cls=stream_cls) + return self._retry_request( + options, + cast_to, + retries, + stream=stream, + stream_cls=stream_cls, + ) raise APITimeoutError(request=request) from err except Exception as err: if retries > 0: - return self._retry_request(options, cast_to, retries, stream=stream, stream_cls=stream_cls) + return self._retry_request( + options, + cast_to, + retries, + stream=stream, + stream_cls=stream_cls, + ) raise APIConnectionError(request=request) from err - if stream: - if stream_cls: - return stream_cls(cast_to=self._extract_stream_chunk_type(stream_cls), response=response, client=self) - - stream_cls = cast("type[_StreamT] | None", self._default_stream_cls) - if stream_cls is None: - raise MissingStreamClassError() - return stream_cls(cast_to=cast_to, response=response, client=self) - - return self._process_response(cast_to=cast_to, options=options, response=response) + return self._process_response( + cast_to=cast_to, + options=options, + response=response, + stream=stream, + stream_cls=stream_cls, + ) def _retry_request( self, @@ -979,13 +927,17 @@ def _request_api_list( page: Type[SyncPageT], options: FinalRequestOptions, ) -> SyncPageT: - resp = self.request(page, options, stream=False) - resp._set_private_attributes( # pyright: ignore[reportPrivateUsage] - client=self, - model=model, - options=options, - ) - return resp + def _parser(resp: SyncPageT) -> SyncPageT: + resp._set_private_attributes( + client=self, + model=model, + options=options, + ) + return resp + + options.post_parser = _parser + + return self.request(page, options, stream=False) @overload def get( @@ -1088,7 +1040,9 @@ def post( stream: bool = False, stream_cls: type[_StreamT] | None = None, ) -> ResponseT | _StreamT: - opts = FinalRequestOptions.construct(method="post", url=path, json_data=body, files=files, **options) + opts = FinalRequestOptions.construct( + method="post", url=path, json_data=body, files=to_httpx_files(files), **options + ) return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) def patch( @@ -1111,7 +1065,9 @@ def put( files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="put", url=path, json_data=body, files=files, **options) + opts = FinalRequestOptions.construct( + method="put", url=path, json_data=body, files=to_httpx_files(files), **options + ) return self.request(cast_to, opts) def delete( @@ -1139,7 +1095,7 @@ def get_api_list( return self._request_api_list(model, page, opts) -class AsyncAPIClient(BaseClient[httpx.AsyncClient]): +class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]): _client: httpx.AsyncClient _has_custom_http_client: bool _default_stream_cls: type[AsyncStream[Any]] | None = None @@ -1349,16 +1305,13 @@ async def _request( return await self._retry_request(options, cast_to, retries, stream=stream, stream_cls=stream_cls) raise APIConnectionError(request=request) from err - if stream: - if stream_cls: - return stream_cls(cast_to=self._extract_stream_chunk_type(stream_cls), response=response, client=self) - - stream_cls = cast("type[_AsyncStreamT] | None", self._default_stream_cls) - if stream_cls is None: - raise MissingStreamClassError() - return stream_cls(cast_to=cast_to, response=response, client=self) - - return self._process_response(cast_to=cast_to, options=options, response=response) + return self._process_response( + cast_to=cast_to, + options=options, + response=response, + stream=stream, + stream_cls=stream_cls, + ) async def _retry_request( self, @@ -1491,7 +1444,9 @@ async def post( stream: bool = False, stream_cls: type[_AsyncStreamT] | None = None, ) -> ResponseT | _AsyncStreamT: - opts = FinalRequestOptions.construct(method="post", url=path, json_data=body, files=files, **options) + opts = FinalRequestOptions.construct( + method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options + ) return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) async def patch( @@ -1514,7 +1469,9 @@ async def put( files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="put", url=path, json_data=body, files=files, **options) + opts = FinalRequestOptions.construct( + method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options + ) return await self.request(cast_to, opts) async def delete( @@ -1551,6 +1508,7 @@ def make_request_options( extra_body: Body | None = None, idempotency_key: str | None = None, timeout: float | None | NotGiven = NOT_GIVEN, + post_parser: PostParser | NotGiven = NOT_GIVEN, ) -> RequestOptions: """Create a dict of type RequestOptions without keys of NotGiven values.""" options: RequestOptions = {} @@ -1572,6 +1530,10 @@ def make_request_options( if idempotency_key is not None: options["idempotency_key"] = idempotency_key + if is_given(post_parser): + # internal + options["post_parser"] = post_parser # type: ignore + return options @@ -1579,6 +1541,7 @@ class OtherPlatform: def __init__(self, name: str) -> None: self.name = name + @override def __str__(self) -> str: return f"Other:{self.name}" @@ -1640,6 +1603,7 @@ class OtherArch: def __init__(self, name: str) -> None: self.name = name + @override def __str__(self) -> str: return f"other:{self.name}" diff --git a/src/finch/_client.py b/src/finch/_client.py index c388a140..7c0f9b38 100644 --- a/src/finch/_client.py +++ b/src/finch/_client.py @@ -5,6 +5,7 @@ import os import asyncio from typing import Union, Mapping +from typing_extensions import override import httpx @@ -52,6 +53,7 @@ class Finch(SyncAPIClient): account: resources.Account webhooks: resources.Webhooks request_forwarding: resources.RequestForwarding + with_raw_response: FinchWithRawResponse # client options access_token: str | None @@ -132,12 +134,15 @@ def __init__( self.account = resources.Account(self) self.webhooks = resources.Webhooks(self) self.request_forwarding = resources.RequestForwarding(self) + self.with_raw_response = FinchWithRawResponse(self) @property + @override def qs(self) -> Querystring: return Querystring(array_format="comma") @property + @override def auth_headers(self) -> dict[str, str]: access_token = self.access_token if access_token is None: @@ -145,6 +150,7 @@ def auth_headers(self) -> dict[str, str]: return {"Authorization": f"Bearer {access_token}"} @property + @override def default_headers(self) -> dict[str, str | Omit]: return { **super().default_headers, @@ -152,6 +158,7 @@ def default_headers(self) -> dict[str, str | Omit]: **self._custom_headers, } + @override def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None: if self.access_token and headers.get("Authorization"): return @@ -308,6 +315,7 @@ def get_auth_url( ) ) + @override def _make_status_error( self, err_msg: str, @@ -347,6 +355,7 @@ class AsyncFinch(AsyncAPIClient): account: resources.AsyncAccount webhooks: resources.AsyncWebhooks request_forwarding: resources.AsyncRequestForwarding + with_raw_response: AsyncFinchWithRawResponse # client options access_token: str | None @@ -427,12 +436,15 @@ def __init__( self.account = resources.AsyncAccount(self) self.webhooks = resources.AsyncWebhooks(self) self.request_forwarding = resources.AsyncRequestForwarding(self) + self.with_raw_response = AsyncFinchWithRawResponse(self) @property + @override def qs(self) -> Querystring: return Querystring(array_format="comma") @property + @override def auth_headers(self) -> dict[str, str]: access_token = self.access_token if access_token is None: @@ -440,6 +452,7 @@ def auth_headers(self) -> dict[str, str]: return {"Authorization": f"Bearer {access_token}"} @property + @override def default_headers(self) -> dict[str, str | Omit]: return { **super().default_headers, @@ -447,6 +460,7 @@ def default_headers(self) -> dict[str, str | Omit]: **self._custom_headers, } + @override def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None: if self.access_token and headers.get("Authorization"): return @@ -606,6 +620,7 @@ def get_auth_url( ) ) + @override def _make_status_error( self, err_msg: str, @@ -639,6 +654,22 @@ def _make_status_error( return APIStatusError(err_msg, response=response, body=body) +class FinchWithRawResponse: + def __init__(self, client: Finch) -> None: + self.hris = resources.HRISWithRawResponse(client.hris) + self.providers = resources.ProvidersWithRawResponse(client.providers) + self.account = resources.AccountWithRawResponse(client.account) + self.request_forwarding = resources.RequestForwardingWithRawResponse(client.request_forwarding) + + +class AsyncFinchWithRawResponse: + def __init__(self, client: AsyncFinch) -> None: + self.hris = resources.AsyncHRISWithRawResponse(client.hris) + self.providers = resources.AsyncProvidersWithRawResponse(client.providers) + self.account = resources.AsyncAccountWithRawResponse(client.account) + self.request_forwarding = resources.AsyncRequestForwardingWithRawResponse(client.request_forwarding) + + Client = Finch AsyncClient = AsyncFinch diff --git a/src/finch/_constants.py b/src/finch/_constants.py new file mode 100644 index 00000000..0c3f31df --- /dev/null +++ b/src/finch/_constants.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. + +import httpx + +RAW_RESPONSE_HEADER = "X-Stainless-Raw-Response" + +# default timeout is 1 minute +DEFAULT_TIMEOUT = httpx.Timeout(timeout=60.0, connect=5.0) +DEFAULT_MAX_RETRIES = 2 +DEFAULT_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20) diff --git a/src/finch/_files.py b/src/finch/_files.py new file mode 100644 index 00000000..b6e8af8b --- /dev/null +++ b/src/finch/_files.py @@ -0,0 +1,122 @@ +from __future__ import annotations + +import io +import os +import pathlib +from typing import overload +from typing_extensions import TypeGuard + +import anyio + +from ._types import ( + FileTypes, + FileContent, + RequestFiles, + HttpxFileTypes, + HttpxFileContent, + HttpxRequestFiles, +) +from ._utils import is_tuple_t, is_mapping_t, is_sequence_t + + +def is_file_content(obj: object) -> TypeGuard[FileContent]: + return ( + isinstance(obj, bytes) or isinstance(obj, tuple) or isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) + ) + + +def assert_is_file_content(obj: object, *, key: str | None = None) -> None: + if not is_file_content(obj): + prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`" + raise RuntimeError( + f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead." + ) from None + + +@overload +def to_httpx_files(files: None) -> None: + ... + + +@overload +def to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: + ... + + +def to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None: + if files is None: + return None + + if is_mapping_t(files): + files = {key: _transform_file(file) for key, file in files.items()} + elif is_sequence_t(files): + files = [(key, _transform_file(file)) for key, file in files] + else: + raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence") + + return files + + +def _transform_file(file: FileTypes) -> HttpxFileTypes: + if is_file_content(file): + if isinstance(file, os.PathLike): + path = pathlib.Path(file) + return (path.name, path.read_bytes()) + + return file + + if is_tuple_t(file): + return (file[0], _read_file_content(file[1]), *file[2:]) + + raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") + + +def _read_file_content(file: FileContent) -> HttpxFileContent: + if isinstance(file, os.PathLike): + return pathlib.Path(file).read_bytes() + return file + + +@overload +async def async_to_httpx_files(files: None) -> None: + ... + + +@overload +async def async_to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: + ... + + +async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None: + if files is None: + return None + + if is_mapping_t(files): + files = {key: await _async_transform_file(file) for key, file in files.items()} + elif is_sequence_t(files): + files = [(key, await _async_transform_file(file)) for key, file in files] + else: + raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence") + + return files + + +async def _async_transform_file(file: FileTypes) -> HttpxFileTypes: + if is_file_content(file): + if isinstance(file, os.PathLike): + path = anyio.Path(file) + return (path.name, await path.read_bytes()) + + return file + + if is_tuple_t(file): + return (file[0], await _async_read_file_content(file[1]), *file[2:]) + + raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") + + +async def _async_read_file_content(file: FileContent) -> HttpxFileContent: + if isinstance(file, os.PathLike): + return await anyio.Path(file).read_bytes() + + return file diff --git a/src/finch/_models.py b/src/finch/_models.py index f663155c..40245ac9 100644 --- a/src/finch/_models.py +++ b/src/finch/_models.py @@ -1,9 +1,19 @@ from __future__ import annotations import inspect -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, cast +from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast from datetime import date, datetime -from typing_extensions import Literal, ClassVar, Protocol, final, runtime_checkable +from typing_extensions import ( + Unpack, + Literal, + ClassVar, + Protocol, + Required, + TypedDict, + final, + override, + runtime_checkable, +) import pydantic import pydantic.generics @@ -18,9 +28,16 @@ Timeout, NotGiven, AnyMapping, - RequestFiles, + HttpxRequestFiles, +) +from ._utils import ( + is_list, + is_given, + is_mapping, + parse_date, + parse_datetime, + strip_not_given, ) -from ._utils import is_list, is_mapping, parse_date, parse_datetime, strip_not_given from ._compat import PYDANTIC_V2, ConfigDict from ._compat import GenericModel as BaseGenericModel from ._compat import ( @@ -33,6 +50,7 @@ get_model_fields, field_get_default, ) +from ._constants import RAW_RESPONSE_HEADER __all__ = ["BaseModel", "GenericModel"] @@ -50,6 +68,7 @@ class BaseModel(pydantic.BaseModel): else: @property + @override def model_fields_set(self) -> set[str]: # a forwards-compat shim for pydantic v2 return self.__fields_set__ # type: ignore @@ -57,6 +76,7 @@ def model_fields_set(self) -> set[str]: class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] extra: Any = pydantic.Extra.allow # type: ignore + @override def __str__(self) -> str: # mypy complains about an invalid self arg return f'{self.__repr_name__()}({self.__repr_str__(", ")})' # type: ignore[misc] @@ -64,6 +84,7 @@ def __str__(self) -> str: # Override the 'construct' method in a way that supports recursive parsing without validation. # Based on https://github.com/samuelcolvin/pydantic/issues/1168#issuecomment-817742836. @classmethod + @override def construct( cls: Type[ModelT], _fields_set: set[str] | None = None, @@ -130,6 +151,7 @@ def construct( # a specifc pydantic version as some users may not know which # pydantic version they are currently using + @override def model_dump( self, *, @@ -178,6 +200,7 @@ def model_dump( exclude_none=exclude_none, ) + @override def model_dump_json( self, *, @@ -363,6 +386,19 @@ def _create_pydantic_model(type_: _T) -> Type[RootModel[_T]]: return RootModel[type_] # type: ignore +class FinalRequestOptionsInput(TypedDict, total=False): + method: Required[str] + url: Required[str] + params: Query + headers: Headers + max_retries: int + timeout: float | Timeout | None + files: HttpxRequestFiles | None + idempotency_key: str + json_data: Body + extra_json: AnyMapping + + @final class FinalRequestOptions(pydantic.BaseModel): method: str @@ -371,8 +407,9 @@ class FinalRequestOptions(pydantic.BaseModel): headers: Union[Headers, NotGiven] = NotGiven() max_retries: Union[int, NotGiven] = NotGiven() timeout: Union[float, Timeout, None, NotGiven] = NotGiven() - files: Union[RequestFiles, None] = None + files: Union[HttpxRequestFiles, None] = None idempotency_key: Union[str, None] = None + post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() # It should be noted that we cannot use `json` here as that would override # a BaseModel method in an incompatible fashion. @@ -391,15 +428,25 @@ def get_max_retries(self, max_retries: int) -> int: return max_retries return self.max_retries + def _strip_raw_response_header(self) -> None: + if not is_given(self.headers): + return + + if self.headers.get(RAW_RESPONSE_HEADER): + self.headers = {**self.headers} + self.headers.pop(RAW_RESPONSE_HEADER) + # override the `construct` method so that we can run custom transformations. # this is necessary as we don't want to do any actual runtime type checking # (which means we can't use validators) but we do want to ensure that `NotGiven` # values are not present + # + # type ignore required because we're adding explicit types to `**values` @classmethod - def construct( + def construct( # type: ignore cls, _fields_set: set[str] | None = None, - **values: Any, + **values: Unpack[FinalRequestOptionsInput], ) -> FinalRequestOptions: kwargs: dict[str, Any] = { # we unconditionally call `strip_not_given` on any value diff --git a/src/finch/_response.py b/src/finch/_response.py new file mode 100644 index 00000000..7939edaa --- /dev/null +++ b/src/finch/_response.py @@ -0,0 +1,249 @@ +from __future__ import annotations + +import inspect +import datetime +import functools +from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast +from typing_extensions import Awaitable, ParamSpec, get_args, override, get_origin + +import httpx +import pydantic + +from ._types import NoneType, UnknownResponse +from ._utils import is_given +from ._models import BaseModel +from ._constants import RAW_RESPONSE_HEADER +from ._exceptions import APIResponseValidationError + +if TYPE_CHECKING: + from ._models import FinalRequestOptions + from ._base_client import Stream, BaseClient, AsyncStream + + +P = ParamSpec("P") +R = TypeVar("R") + + +class APIResponse(Generic[R]): + _cast_to: type[R] + _client: BaseClient[Any, Any] + _parsed: R | None + _stream: bool + _stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None + _options: FinalRequestOptions + + http_response: httpx.Response + + def __init__( + self, + *, + raw: httpx.Response, + cast_to: type[R], + client: BaseClient[Any, Any], + stream: bool, + stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, + options: FinalRequestOptions, + ) -> None: + self._cast_to = cast_to + self._client = client + self._parsed = None + self._stream = stream + self._stream_cls = stream_cls + self._options = options + self.http_response = raw + + def parse(self) -> R: + if self._parsed is not None: + return self._parsed + + parsed = self._parse() + if is_given(self._options.post_parser): + parsed = self._options.post_parser(parsed) + + self._parsed = parsed + return parsed + + @property + def headers(self) -> httpx.Headers: + return self.http_response.headers + + @property + def http_request(self) -> httpx.Request: + return self.http_response.request + + @property + def status_code(self) -> int: + return self.http_response.status_code + + @property + def url(self) -> httpx.URL: + return self.http_response.url + + @property + def method(self) -> str: + return self.http_request.method + + @property + def content(self) -> bytes: + return self.http_response.content + + @property + def text(self) -> str: + return self.http_response.text + + @property + def http_version(self) -> str: + return self.http_response.http_version + + @property + def elapsed(self) -> datetime.timedelta: + """The time taken for the complete request/response cycle to complete.""" + return self.http_response.elapsed + + def _parse(self) -> R: + if self._stream: + if self._stream_cls: + return cast( + R, + self._stream_cls( + cast_to=_extract_stream_chunk_type(self._stream_cls), + response=self.http_response, + client=cast(Any, self._client), + ), + ) + + stream_cls = cast("type[Stream[Any]] | type[AsyncStream[Any]] | None", self._client._default_stream_cls) + if stream_cls is None: + raise MissingStreamClassError() + + return cast( + R, + stream_cls( + cast_to=self._cast_to, + response=self.http_response, + client=cast(Any, self._client), + ), + ) + + cast_to = self._cast_to + if cast_to is NoneType: + return cast(R, None) + + response = self.http_response + if cast_to == str: + return cast(R, response.text) + + origin = get_origin(cast_to) or cast_to + + if origin == APIResponse: + raise RuntimeError("Unexpected state - cast_to is `APIResponse`") + + if inspect.isclass(origin) and issubclass(origin, httpx.Response): + # Because of the invariance of our ResponseT TypeVar, users can subclass httpx.Response + # and pass that class to our request functions. We cannot change the variance to be either + # covariant or contravariant as that makes our usage of ResponseT illegal. We could construct + # the response class ourselves but that is something that should be supported directly in httpx + # as it would be easy to incorrectly construct the Response object due to the multitude of arguments. + if cast_to != httpx.Response: + raise ValueError(f"Subclasses of httpx.Response cannot be passed to `cast_to`") + return cast(R, response) + + # The check here is necessary as we are subverting the the type system + # with casts as the relationship between TypeVars and Types are very strict + # which means we must return *exactly* what was input or transform it in a + # way that retains the TypeVar state. As we cannot do that in this function + # then we have to resort to using `cast`. At the time of writing, we know this + # to be safe as we have handled all the types that could be bound to the + # `ResponseT` TypeVar, however if that TypeVar is ever updated in the future, then + # this function would become unsafe but a type checker would not report an error. + if ( + cast_to is not UnknownResponse + and not origin is list + and not origin is dict + and not origin is Union + and not issubclass(origin, BaseModel) + ): + raise RuntimeError( + f"Invalid state, expected {cast_to} to be a subclass type of {BaseModel}, {dict}, {list} or {Union}." + ) + + # split is required to handle cases where additional information is included + # in the response, e.g. application/json; charset=utf-8 + content_type, *_ = response.headers.get("content-type").split(";") + if content_type != "application/json": + if self._client._strict_response_validation: + raise APIResponseValidationError( + response=response, + message=f"Expected Content-Type response header to be `application/json` but received `{content_type}` instead.", + body=response.text, + ) + + # If the API responds with content that isn't JSON then we just return + # the (decoded) text without performing any parsing so that you can still + # handle the response however you need to. + return response.text # type: ignore + + data = response.json() + + try: + return self._client._process_response_data( + data=data, + cast_to=cast_to, # type: ignore + response=response, + ) + except pydantic.ValidationError as err: + raise APIResponseValidationError(response=response, body=data) from err + + @override + def __repr__(self) -> str: + return f"" + + +class MissingStreamClassError(TypeError): + def __init__(self) -> None: + super().__init__( + "The `stream` argument was set to `True` but the `stream_cls` argument was not given. See `finch._streaming` for reference", + ) + + +def _extract_stream_chunk_type(stream_cls: type) -> type: + args = get_args(stream_cls) + if not args: + raise TypeError( + f"Expected stream_cls to have been given a generic type argument, e.g. Stream[Foo] but received {stream_cls}", + ) + return cast(type, args[0]) + + +def to_raw_response_wrapper(func: Callable[P, R]) -> Callable[P, APIResponse[R]]: + """Higher order function that takes one of our bound API methods and wraps it + to support returning the raw `APIResponse` object directly. + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> APIResponse[R]: + extra_headers = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "true" + + kwargs["extra_headers"] = extra_headers + + return cast(APIResponse[R], func(*args, **kwargs)) + + return wrapped + + +def async_to_raw_response_wrapper(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[APIResponse[R]]]: + """Higher order function that takes one of our bound API methods and wraps it + to support returning the raw `APIResponse` object directly. + """ + + @functools.wraps(func) + async def wrapped(*args: P.args, **kwargs: P.kwargs) -> APIResponse[R]: + extra_headers = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "true" + + kwargs["extra_headers"] = extra_headers + + return cast(APIResponse[R], await func(*args, **kwargs)) + + return wrapped diff --git a/src/finch/_streaming.py b/src/finch/_streaming.py index 18749b53..b0600fc9 100644 --- a/src/finch/_streaming.py +++ b/src/finch/_streaming.py @@ -3,6 +3,7 @@ import json from typing import TYPE_CHECKING, Any, Generic, Iterator, AsyncIterator +from typing_extensions import override import httpx @@ -123,6 +124,7 @@ def data(self) -> str: def json(self) -> Any: return json.loads(self.data) + @override def __repr__(self) -> str: return f"ServerSentEvent(event={self.event}, data={self.data}, id={self.id}, retry={self.retry})" diff --git a/src/finch/_types.py b/src/finch/_types.py index 45e89382..ef5e84c5 100644 --- a/src/finch/_types.py +++ b/src/finch/_types.py @@ -1,5 +1,6 @@ from __future__ import annotations +from os import PathLike from typing import ( IO, TYPE_CHECKING, @@ -11,10 +12,18 @@ Union, Mapping, TypeVar, + Callable, Optional, Sequence, ) -from typing_extensions import Literal, Protocol, TypeAlias, TypedDict, runtime_checkable +from typing_extensions import ( + Literal, + Protocol, + TypeAlias, + TypedDict, + override, + runtime_checkable, +) import httpx import pydantic @@ -32,9 +41,10 @@ _T = TypeVar("_T") # Approximates httpx internal ProxiesTypes and RequestFiles types +# while adding support for `PathLike` instances ProxiesDict = Dict["str | URL", Union[None, str, URL, Proxy]] ProxiesTypes = Union[str, Proxy, ProxiesDict] -FileContent = Union[IO[bytes], bytes] +FileContent = Union[IO[bytes], bytes, PathLike[str]] FileTypes = Union[ # file (or bytes) FileContent, @@ -47,6 +57,19 @@ ] RequestFiles = Union[Mapping[str, FileTypes], Sequence[Tuple[str, FileTypes]]] +# duplicate of the above but without our custom file support +HttpxFileContent = Union[IO[bytes], bytes] +HttpxFileTypes = Union[ + # file (or bytes) + HttpxFileContent, + # (filename, file (or bytes)) + Tuple[Optional[str], HttpxFileContent], + # (filename, file (or bytes), content_type) + Tuple[Optional[str], HttpxFileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + Tuple[Optional[str], HttpxFileContent, Optional[str], Mapping[str, str]], +] +HttpxRequestFiles = Union[Mapping[str, HttpxFileTypes], Sequence[Tuple[str, HttpxFileTypes]]] # Workaround to support (cast_to: Type[ResponseT]) -> ResponseT # where ResponseT includes `None`. In order to support directly @@ -104,6 +127,7 @@ def get(timeout: Union[int, NotGiven, None] = NotGiven()) -> Response: ... def __bool__(self) -> Literal[False]: return False + @override def __repr__(self) -> str: return "NOT_GIVEN" @@ -165,3 +189,5 @@ def get(self, __key: str) -> str | None: # Note: copied from Pydantic # https://github.com/pydantic/pydantic/blob/32ea570bf96e84234d2992e1ddf40ab8a565925a/pydantic/main.py#L49 IncEx: TypeAlias = "set[int] | set[str] | dict[int, Any] | dict[str, Any] | None" + +PostParser = Callable[[Any], Any] diff --git a/src/finch/_utils/__init__.py b/src/finch/_utils/__init__.py index 26dc560b..d3397212 100644 --- a/src/finch/_utils/__init__.py +++ b/src/finch/_utils/__init__.py @@ -3,13 +3,18 @@ from ._utils import is_dict as is_dict from ._utils import is_list as is_list from ._utils import is_given as is_given +from ._utils import is_tuple as is_tuple from ._utils import is_mapping as is_mapping +from ._utils import is_tuple_t as is_tuple_t from ._utils import parse_date as parse_date +from ._utils import is_sequence as is_sequence from ._utils import coerce_float as coerce_float from ._utils import is_list_type as is_list_type +from ._utils import is_mapping_t as is_mapping_t from ._utils import removeprefix as removeprefix from ._utils import removesuffix as removesuffix from ._utils import extract_files as extract_files +from ._utils import is_sequence_t as is_sequence_t from ._utils import is_union_type as is_union_type from ._utils import required_args as required_args from ._utils import coerce_boolean as coerce_boolean diff --git a/src/finch/_utils/_proxy.py b/src/finch/_utils/_proxy.py index fd85ebd5..aa934a3f 100644 --- a/src/finch/_utils/_proxy.py +++ b/src/finch/_utils/_proxy.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod from typing import Generic, TypeVar, Iterable, cast -from typing_extensions import ClassVar +from typing_extensions import ClassVar, override T = TypeVar("T") @@ -21,16 +21,20 @@ def __init__(self) -> None: def __getattr__(self, attr: str) -> object: return getattr(self.__get_proxied__(), attr) + @override def __repr__(self) -> str: return repr(self.__get_proxied__()) + @override def __str__(self) -> str: return str(self.__get_proxied__()) + @override def __dir__(self) -> Iterable[str]: return self.__get_proxied__().__dir__() @property # type: ignore + @override def __class__(self) -> type: return self.__get_proxied__().__class__ diff --git a/src/finch/_utils/_transform.py b/src/finch/_utils/_transform.py index c007d8b0..d524b329 100644 --- a/src/finch/_utils/_transform.py +++ b/src/finch/_utils/_transform.py @@ -2,7 +2,7 @@ from typing import Any, List, Mapping, TypeVar, cast from datetime import date, datetime -from typing_extensions import Literal, get_args, get_type_hints +from typing_extensions import Literal, get_args, override, get_type_hints from ._utils import ( is_list, @@ -52,6 +52,7 @@ def __init__( self.format = format self.format_template = format_template + @override def __repr__(self) -> str: return f"{self.__class__.__name__}(alias='{self.alias}', format={self.format}, format_template='{self.format_template}')" diff --git a/src/finch/_utils/_utils.py b/src/finch/_utils/_utils.py index cb660d16..4b51dcb2 100644 --- a/src/finch/_utils/_utils.py +++ b/src/finch/_utils/_utils.py @@ -1,11 +1,20 @@ from __future__ import annotations -import io import os import re import inspect import functools -from typing import Any, Mapping, TypeVar, Callable, Iterable, Sequence, cast, overload +from typing import ( + Any, + Tuple, + Mapping, + TypeVar, + Callable, + Iterable, + Sequence, + cast, + overload, +) from pathlib import Path from typing_extensions import Required, Annotated, TypeGuard, get_args, get_origin @@ -15,6 +24,9 @@ from .._compat import parse_datetime as parse_datetime _T = TypeVar("_T") +_TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) +_MappingT = TypeVar("_MappingT", bound=Mapping[str, object]) +_SequenceT = TypeVar("_SequenceT", bound=Sequence[object]) CallableT = TypeVar("CallableT", bound=Callable[..., Any]) @@ -55,13 +67,11 @@ def _extract_items( # no value was provided - we can safely ignore return [] - # We have exhausted the path, return the entry we found. - if not isinstance(obj, bytes) and not isinstance(obj, tuple) and not isinstance(obj, io.IOBase): - raise RuntimeError( - f"Expected entry at {flattened_key} to be bytes, an io.IOBase instance or a tuple but received {type(obj)} instead." - ) from None + # cyclical import + from .._files import assert_is_file_content - # TODO: validate obj more? + # We have exhausted the path, return the entry we found. + assert_is_file_content(obj, key=flattened_key) assert flattened_key is not None return [(flattened_key, cast(FileTypes, obj))] @@ -116,12 +126,36 @@ def is_given(obj: NotGivenOr[_T]) -> TypeGuard[_T]: # The default narrowing for isinstance(obj, dict) is dict[unknown, unknown], # however this cause Pyright to rightfully report errors. As we know we don't # care about the contained types we can safely use `object` in it's place. +# +# There are two separate functions defined, `is_*` and `is_*_t` for different use cases. +# `is_*` is for when you're dealing with an unknown input +# `is_*_t` is for when you're narrowing a known union type to a specific subset + + +def is_tuple(obj: object) -> TypeGuard[tuple[object, ...]]: + return isinstance(obj, tuple) + + +def is_tuple_t(obj: _TupleT | object) -> TypeGuard[_TupleT]: + return isinstance(obj, tuple) + + +def is_sequence(obj: object) -> TypeGuard[Sequence[object]]: + return isinstance(obj, Sequence) + + +def is_sequence_t(obj: _SequenceT | object) -> TypeGuard[_SequenceT]: + return isinstance(obj, Sequence) def is_mapping(obj: object) -> TypeGuard[Mapping[str, object]]: return isinstance(obj, Mapping) +def is_mapping_t(obj: _MappingT | object) -> TypeGuard[_MappingT]: + return isinstance(obj, Mapping) + + def is_dict(obj: object) -> TypeGuard[dict[object, object]]: return isinstance(obj, dict) diff --git a/src/finch/_version.py b/src/finch/_version.py index d58b9bc3..78ac8c5f 100644 --- a/src/finch/_version.py +++ b/src/finch/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. __title__ = "finch" -__version__ = "0.2.3" # x-release-please-version +__version__ = "0.3.0" # x-release-please-version diff --git a/src/finch/pagination.py b/src/finch/pagination.py index dc050d24..8162b275 100644 --- a/src/finch/pagination.py +++ b/src/finch/pagination.py @@ -1,6 +1,7 @@ # File generated from our OpenAPI spec by Stainless. from typing import Any, List, Type, Generic, Mapping, TypeVar, Optional, cast +from typing_extensions import override from httpx import Response @@ -25,9 +26,11 @@ class SyncSinglePage(BaseSyncPage[ModelT], BasePage[ModelT], Generic[ModelT]): items: List[ModelT] + @override def _get_page_items(self) -> List[ModelT]: return self.items + @override def next_page_info(self) -> None: """ This page represents a response that isn't actually paginated at the API level @@ -47,9 +50,11 @@ def build(cls: Type[_BaseModelT], *, response: Response, data: object) -> _BaseM class AsyncSinglePage(BaseAsyncPage[ModelT], BasePage[ModelT], Generic[ModelT]): items: List[ModelT] + @override def _get_page_items(self) -> List[ModelT]: return self.items + @override def next_page_info(self) -> None: """ This page represents a response that isn't actually paginated at the API level @@ -69,9 +74,11 @@ def build(cls: Type[_BaseModelT], *, response: Response, data: object) -> _BaseM class SyncResponsesPage(BaseSyncPage[ModelT], BasePage[ModelT], Generic[ModelT]): responses: List[ModelT] + @override def _get_page_items(self) -> List[ModelT]: return self.responses + @override def next_page_info(self) -> None: """ This page represents a response that isn't actually paginated at the API level @@ -83,9 +90,11 @@ def next_page_info(self) -> None: class AsyncResponsesPage(BaseAsyncPage[ModelT], BasePage[ModelT], Generic[ModelT]): responses: List[ModelT] + @override def _get_page_items(self) -> List[ModelT]: return self.responses + @override def next_page_info(self) -> None: """ This page represents a response that isn't actually paginated at the API level @@ -102,9 +111,11 @@ class SyncIndividualsPage(BaseSyncPage[ModelT], BasePage[ModelT], Generic[ModelT individuals: List[ModelT] paging: Paging + @override def _get_page_items(self) -> List[ModelT]: return self.individuals + @override def next_page_info(self) -> Optional[PageInfo]: offset = self.paging.offset if offset is None: @@ -127,9 +138,11 @@ class AsyncIndividualsPage(BaseAsyncPage[ModelT], BasePage[ModelT], Generic[Mode individuals: List[ModelT] paging: Paging + @override def _get_page_items(self) -> List[ModelT]: return self.individuals + @override def next_page_info(self) -> Optional[PageInfo]: offset = self.paging.offset if offset is None: diff --git a/src/finch/resources/__init__.py b/src/finch/resources/__init__.py index 8e71749e..b87ddc17 100644 --- a/src/finch/resources/__init__.py +++ b/src/finch/resources/__init__.py @@ -1,20 +1,43 @@ # File generated from our OpenAPI spec by Stainless. -from .hris import HRIS, AsyncHRIS -from .account import Account, AsyncAccount +from .hris import HRIS, AsyncHRIS, HRISWithRawResponse, AsyncHRISWithRawResponse +from .account import ( + Account, + AsyncAccount, + AccountWithRawResponse, + AsyncAccountWithRawResponse, +) from .webhooks import Webhooks, AsyncWebhooks -from .providers import Providers, AsyncProviders -from .request_forwarding import RequestForwarding, AsyncRequestForwarding +from .providers import ( + Providers, + AsyncProviders, + ProvidersWithRawResponse, + AsyncProvidersWithRawResponse, +) +from .request_forwarding import ( + RequestForwarding, + AsyncRequestForwarding, + RequestForwardingWithRawResponse, + AsyncRequestForwardingWithRawResponse, +) __all__ = [ "HRIS", "AsyncHRIS", + "HRISWithRawResponse", + "AsyncHRISWithRawResponse", "Providers", "AsyncProviders", + "ProvidersWithRawResponse", + "AsyncProvidersWithRawResponse", "Account", "AsyncAccount", + "AccountWithRawResponse", + "AsyncAccountWithRawResponse", "Webhooks", "AsyncWebhooks", "RequestForwarding", "AsyncRequestForwarding", + "RequestForwardingWithRawResponse", + "AsyncRequestForwardingWithRawResponse", ] diff --git a/src/finch/resources/account.py b/src/finch/resources/account.py index db678453..18fe7744 100644 --- a/src/finch/resources/account.py +++ b/src/finch/resources/account.py @@ -2,15 +2,27 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from ..types import Introspection, DisconnectResponse from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import to_raw_response_wrapper, async_to_raw_response_wrapper from .._base_client import make_request_options +if TYPE_CHECKING: + from .._client import Finch, AsyncFinch + __all__ = ["Account", "AsyncAccount"] class Account(SyncAPIResource): + with_raw_response: AccountWithRawResponse + + def __init__(self, client: Finch) -> None: + super().__init__(client) + self.with_raw_response = AccountWithRawResponse(self) + def disconnect( self, *, @@ -55,6 +67,12 @@ def introspect( class AsyncAccount(AsyncAPIResource): + with_raw_response: AsyncAccountWithRawResponse + + def __init__(self, client: AsyncFinch) -> None: + super().__init__(client) + self.with_raw_response = AsyncAccountWithRawResponse(self) + async def disconnect( self, *, @@ -96,3 +114,23 @@ async def introspect( ), cast_to=Introspection, ) + + +class AccountWithRawResponse: + def __init__(self, account: Account) -> None: + self.disconnect = to_raw_response_wrapper( + account.disconnect, + ) + self.introspect = to_raw_response_wrapper( + account.introspect, + ) + + +class AsyncAccountWithRawResponse: + def __init__(self, account: AsyncAccount) -> None: + self.disconnect = async_to_raw_response_wrapper( + account.disconnect, + ) + self.introspect = async_to_raw_response_wrapper( + account.introspect, + ) diff --git a/src/finch/resources/hris/__init__.py b/src/finch/resources/hris/__init__.py index 93bb3d45..4659abb9 100644 --- a/src/finch/resources/hris/__init__.py +++ b/src/finch/resources/hris/__init__.py @@ -1,29 +1,80 @@ # File generated from our OpenAPI spec by Stainless. -from .hris import HRIS, AsyncHRIS -from .company import CompanyResource, AsyncCompanyResource -from .benefits import Benefits, AsyncBenefits -from .payments import Payments, AsyncPayments -from .directory import Directory, AsyncDirectory -from .employments import Employments, AsyncEmployments -from .individuals import Individuals, AsyncIndividuals -from .pay_statements import PayStatements, AsyncPayStatements +from .hris import HRIS, AsyncHRIS, HRISWithRawResponse, AsyncHRISWithRawResponse +from .company import ( + CompanyResource, + AsyncCompanyResource, + CompanyResourceWithRawResponse, + AsyncCompanyResourceWithRawResponse, +) +from .benefits import ( + Benefits, + AsyncBenefits, + BenefitsWithRawResponse, + AsyncBenefitsWithRawResponse, +) +from .payments import ( + Payments, + AsyncPayments, + PaymentsWithRawResponse, + AsyncPaymentsWithRawResponse, +) +from .directory import ( + Directory, + AsyncDirectory, + DirectoryWithRawResponse, + AsyncDirectoryWithRawResponse, +) +from .employments import ( + Employments, + AsyncEmployments, + EmploymentsWithRawResponse, + AsyncEmploymentsWithRawResponse, +) +from .individuals import ( + Individuals, + AsyncIndividuals, + IndividualsWithRawResponse, + AsyncIndividualsWithRawResponse, +) +from .pay_statements import ( + PayStatements, + AsyncPayStatements, + PayStatementsWithRawResponse, + AsyncPayStatementsWithRawResponse, +) __all__ = [ "CompanyResource", "AsyncCompanyResource", + "CompanyResourceWithRawResponse", + "AsyncCompanyResourceWithRawResponse", "Directory", "AsyncDirectory", + "DirectoryWithRawResponse", + "AsyncDirectoryWithRawResponse", "Individuals", "AsyncIndividuals", + "IndividualsWithRawResponse", + "AsyncIndividualsWithRawResponse", "Employments", "AsyncEmployments", + "EmploymentsWithRawResponse", + "AsyncEmploymentsWithRawResponse", "Payments", "AsyncPayments", + "PaymentsWithRawResponse", + "AsyncPaymentsWithRawResponse", "PayStatements", "AsyncPayStatements", + "PayStatementsWithRawResponse", + "AsyncPayStatementsWithRawResponse", "Benefits", "AsyncBenefits", + "BenefitsWithRawResponse", + "AsyncBenefitsWithRawResponse", "HRIS", "AsyncHRIS", + "HRISWithRawResponse", + "AsyncHRISWithRawResponse", ] diff --git a/src/finch/resources/hris/benefits/__init__.py b/src/finch/resources/hris/benefits/__init__.py index d1f80294..fbbe99ea 100644 --- a/src/finch/resources/hris/benefits/__init__.py +++ b/src/finch/resources/hris/benefits/__init__.py @@ -1,6 +1,25 @@ # File generated from our OpenAPI spec by Stainless. -from .benefits import Benefits, AsyncBenefits -from .individuals import Individuals, AsyncIndividuals +from .benefits import ( + Benefits, + AsyncBenefits, + BenefitsWithRawResponse, + AsyncBenefitsWithRawResponse, +) +from .individuals import ( + Individuals, + AsyncIndividuals, + IndividualsWithRawResponse, + AsyncIndividualsWithRawResponse, +) -__all__ = ["Individuals", "AsyncIndividuals", "Benefits", "AsyncBenefits"] +__all__ = [ + "Individuals", + "AsyncIndividuals", + "IndividualsWithRawResponse", + "AsyncIndividualsWithRawResponse", + "Benefits", + "AsyncBenefits", + "BenefitsWithRawResponse", + "AsyncBenefitsWithRawResponse", +] diff --git a/src/finch/resources/hris/benefits/benefits.py b/src/finch/resources/hris/benefits/benefits.py index 1068dab2..c48dd78d 100644 --- a/src/finch/resources/hris/benefits/benefits.py +++ b/src/finch/resources/hris/benefits/benefits.py @@ -6,8 +6,14 @@ from ...._types import NOT_GIVEN, Body, Query, Headers, NotGiven from ...._utils import maybe_transform -from .individuals import Individuals, AsyncIndividuals +from .individuals import ( + Individuals, + AsyncIndividuals, + IndividualsWithRawResponse, + AsyncIndividualsWithRawResponse, +) from ...._resource import SyncAPIResource, AsyncAPIResource +from ...._response import to_raw_response_wrapper, async_to_raw_response_wrapper from ....pagination import SyncSinglePage, AsyncSinglePage from ....types.hris import ( BenefitType, @@ -29,10 +35,12 @@ class Benefits(SyncAPIResource): individuals: Individuals + with_raw_response: BenefitsWithRawResponse def __init__(self, client: Finch) -> None: super().__init__(client) self.individuals = Individuals(client) + self.with_raw_response = BenefitsWithRawResponse(self) def create( self, @@ -202,10 +210,12 @@ def list_supported_benefits( class AsyncBenefits(AsyncAPIResource): individuals: AsyncIndividuals + with_raw_response: AsyncBenefitsWithRawResponse def __init__(self, client: AsyncFinch) -> None: super().__init__(client) self.individuals = AsyncIndividuals(client) + self.with_raw_response = AsyncBenefitsWithRawResponse(self) async def create( self, @@ -371,3 +381,45 @@ def list_supported_benefits( ), model=SupportedBenefit, ) + + +class BenefitsWithRawResponse: + def __init__(self, benefits: Benefits) -> None: + self.individuals = IndividualsWithRawResponse(benefits.individuals) + + self.create = to_raw_response_wrapper( + benefits.create, + ) + self.retrieve = to_raw_response_wrapper( + benefits.retrieve, + ) + self.update = to_raw_response_wrapper( + benefits.update, + ) + self.list = to_raw_response_wrapper( + benefits.list, + ) + self.list_supported_benefits = to_raw_response_wrapper( + benefits.list_supported_benefits, + ) + + +class AsyncBenefitsWithRawResponse: + def __init__(self, benefits: AsyncBenefits) -> None: + self.individuals = AsyncIndividualsWithRawResponse(benefits.individuals) + + self.create = async_to_raw_response_wrapper( + benefits.create, + ) + self.retrieve = async_to_raw_response_wrapper( + benefits.retrieve, + ) + self.update = async_to_raw_response_wrapper( + benefits.update, + ) + self.list = async_to_raw_response_wrapper( + benefits.list, + ) + self.list_supported_benefits = async_to_raw_response_wrapper( + benefits.list_supported_benefits, + ) diff --git a/src/finch/resources/hris/benefits/individuals.py b/src/finch/resources/hris/benefits/individuals.py index 7e41e237..8219ab1d 100644 --- a/src/finch/resources/hris/benefits/individuals.py +++ b/src/finch/resources/hris/benefits/individuals.py @@ -2,11 +2,12 @@ from __future__ import annotations -from typing import List +from typing import TYPE_CHECKING, List from ...._types import NOT_GIVEN, Body, Query, Headers, NotGiven from ...._utils import maybe_transform from ...._resource import SyncAPIResource, AsyncAPIResource +from ...._response import to_raw_response_wrapper, async_to_raw_response_wrapper from ....pagination import SyncSinglePage, AsyncSinglePage from ...._base_client import AsyncPaginator, make_request_options from ....types.hris.benefits import ( @@ -19,10 +20,19 @@ individual_retrieve_many_benefits_params, ) +if TYPE_CHECKING: + from ...._client import Finch, AsyncFinch + __all__ = ["Individuals", "AsyncIndividuals"] class Individuals(SyncAPIResource): + with_raw_response: IndividualsWithRawResponse + + def __init__(self, client: Finch) -> None: + super().__init__(client) + self.with_raw_response = IndividualsWithRawResponse(self) + def enroll_many( self, benefit_id: str, @@ -190,6 +200,12 @@ def unenroll_many( class AsyncIndividuals(AsyncAPIResource): + with_raw_response: AsyncIndividualsWithRawResponse + + def __init__(self, client: AsyncFinch) -> None: + super().__init__(client) + self.with_raw_response = AsyncIndividualsWithRawResponse(self) + def enroll_many( self, benefit_id: str, @@ -354,3 +370,35 @@ def unenroll_many( model=UnenrolledIndividual, method="delete", ) + + +class IndividualsWithRawResponse: + def __init__(self, individuals: Individuals) -> None: + self.enroll_many = to_raw_response_wrapper( + individuals.enroll_many, + ) + self.enrolled_ids = to_raw_response_wrapper( + individuals.enrolled_ids, + ) + self.retrieve_many_benefits = to_raw_response_wrapper( + individuals.retrieve_many_benefits, + ) + self.unenroll_many = to_raw_response_wrapper( + individuals.unenroll_many, + ) + + +class AsyncIndividualsWithRawResponse: + def __init__(self, individuals: AsyncIndividuals) -> None: + self.enroll_many = async_to_raw_response_wrapper( + individuals.enroll_many, + ) + self.enrolled_ids = async_to_raw_response_wrapper( + individuals.enrolled_ids, + ) + self.retrieve_many_benefits = async_to_raw_response_wrapper( + individuals.retrieve_many_benefits, + ) + self.unenroll_many = async_to_raw_response_wrapper( + individuals.unenroll_many, + ) diff --git a/src/finch/resources/hris/company.py b/src/finch/resources/hris/company.py index 30de8618..651c153d 100644 --- a/src/finch/resources/hris/company.py +++ b/src/finch/resources/hris/company.py @@ -2,15 +2,27 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import to_raw_response_wrapper, async_to_raw_response_wrapper from ...types.hris import Company from ..._base_client import make_request_options +if TYPE_CHECKING: + from ..._client import Finch, AsyncFinch + __all__ = ["CompanyResource", "AsyncCompanyResource"] class CompanyResource(SyncAPIResource): + with_raw_response: CompanyResourceWithRawResponse + + def __init__(self, client: Finch) -> None: + super().__init__(client) + self.with_raw_response = CompanyResourceWithRawResponse(self) + def retrieve( self, *, @@ -32,6 +44,12 @@ def retrieve( class AsyncCompanyResource(AsyncAPIResource): + with_raw_response: AsyncCompanyResourceWithRawResponse + + def __init__(self, client: AsyncFinch) -> None: + super().__init__(client) + self.with_raw_response = AsyncCompanyResourceWithRawResponse(self) + async def retrieve( self, *, @@ -50,3 +68,17 @@ async def retrieve( ), cast_to=Company, ) + + +class CompanyResourceWithRawResponse: + def __init__(self, company: CompanyResource) -> None: + self.retrieve = to_raw_response_wrapper( + company.retrieve, + ) + + +class AsyncCompanyResourceWithRawResponse: + def __init__(self, company: AsyncCompanyResource) -> None: + self.retrieve = async_to_raw_response_wrapper( + company.retrieve, + ) diff --git a/src/finch/resources/hris/directory.py b/src/finch/resources/hris/directory.py index a13a1c3a..69c39ac5 100644 --- a/src/finch/resources/hris/directory.py +++ b/src/finch/resources/hris/directory.py @@ -3,18 +3,29 @@ from __future__ import annotations import typing_extensions +from typing import TYPE_CHECKING from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven from ..._utils import maybe_transform from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import to_raw_response_wrapper, async_to_raw_response_wrapper from ...pagination import SyncIndividualsPage, AsyncIndividualsPage from ...types.hris import IndividualInDirectory, directory_list_params from ..._base_client import AsyncPaginator, make_request_options +if TYPE_CHECKING: + from ..._client import Finch, AsyncFinch + __all__ = ["Directory", "AsyncDirectory"] class Directory(SyncAPIResource): + with_raw_response: DirectoryWithRawResponse + + def __init__(self, client: Finch) -> None: + super().__init__(client) + self.with_raw_response = DirectoryWithRawResponse(self) + def list( self, *, @@ -102,6 +113,12 @@ def list_individuals( class AsyncDirectory(AsyncAPIResource): + with_raw_response: AsyncDirectoryWithRawResponse + + def __init__(self, client: AsyncFinch) -> None: + super().__init__(client) + self.with_raw_response = AsyncDirectoryWithRawResponse(self) + def list( self, *, @@ -186,3 +203,23 @@ def list_individuals( extra_body=extra_body, timeout=timeout, ) + + +class DirectoryWithRawResponse: + def __init__(self, directory: Directory) -> None: + self.list = to_raw_response_wrapper( + directory.list, + ) + self.list_individuals = to_raw_response_wrapper( # pyright: ignore[reportDeprecated] + directory.list_individuals # pyright: ignore[reportDeprecated], + ) + + +class AsyncDirectoryWithRawResponse: + def __init__(self, directory: AsyncDirectory) -> None: + self.list = async_to_raw_response_wrapper( + directory.list, + ) + self.list_individuals = async_to_raw_response_wrapper( # pyright: ignore[reportDeprecated] + directory.list_individuals # pyright: ignore[reportDeprecated], + ) diff --git a/src/finch/resources/hris/employments.py b/src/finch/resources/hris/employments.py index e724bfd5..d41a531e 100644 --- a/src/finch/resources/hris/employments.py +++ b/src/finch/resources/hris/employments.py @@ -2,19 +2,29 @@ from __future__ import annotations -from typing import List +from typing import TYPE_CHECKING, List from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven from ..._utils import maybe_transform from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import to_raw_response_wrapper, async_to_raw_response_wrapper from ...pagination import SyncResponsesPage, AsyncResponsesPage from ...types.hris import EmploymentDataResponse, employment_retrieve_many_params from ..._base_client import AsyncPaginator, make_request_options +if TYPE_CHECKING: + from ..._client import Finch, AsyncFinch + __all__ = ["Employments", "AsyncEmployments"] class Employments(SyncAPIResource): + with_raw_response: EmploymentsWithRawResponse + + def __init__(self, client: Finch) -> None: + super().__init__(client) + self.with_raw_response = EmploymentsWithRawResponse(self) + def retrieve_many( self, *, @@ -57,6 +67,12 @@ def retrieve_many( class AsyncEmployments(AsyncAPIResource): + with_raw_response: AsyncEmploymentsWithRawResponse + + def __init__(self, client: AsyncFinch) -> None: + super().__init__(client) + self.with_raw_response = AsyncEmploymentsWithRawResponse(self) + def retrieve_many( self, *, @@ -96,3 +112,17 @@ def retrieve_many( model=EmploymentDataResponse, method="post", ) + + +class EmploymentsWithRawResponse: + def __init__(self, employments: Employments) -> None: + self.retrieve_many = to_raw_response_wrapper( + employments.retrieve_many, + ) + + +class AsyncEmploymentsWithRawResponse: + def __init__(self, employments: AsyncEmployments) -> None: + self.retrieve_many = async_to_raw_response_wrapper( + employments.retrieve_many, + ) diff --git a/src/finch/resources/hris/hris.py b/src/finch/resources/hris/hris.py index de98f609..8cd4f554 100644 --- a/src/finch/resources/hris/hris.py +++ b/src/finch/resources/hris/hris.py @@ -4,14 +4,49 @@ from typing import TYPE_CHECKING -from .company import CompanyResource, AsyncCompanyResource -from .benefits import Benefits, AsyncBenefits -from .payments import Payments, AsyncPayments -from .directory import Directory, AsyncDirectory +from .company import ( + CompanyResource, + AsyncCompanyResource, + CompanyResourceWithRawResponse, + AsyncCompanyResourceWithRawResponse, +) +from .benefits import ( + Benefits, + AsyncBenefits, + BenefitsWithRawResponse, + AsyncBenefitsWithRawResponse, +) +from .payments import ( + Payments, + AsyncPayments, + PaymentsWithRawResponse, + AsyncPaymentsWithRawResponse, +) +from .directory import ( + Directory, + AsyncDirectory, + DirectoryWithRawResponse, + AsyncDirectoryWithRawResponse, +) from ..._resource import SyncAPIResource, AsyncAPIResource -from .employments import Employments, AsyncEmployments -from .individuals import Individuals, AsyncIndividuals -from .pay_statements import PayStatements, AsyncPayStatements +from .employments import ( + Employments, + AsyncEmployments, + EmploymentsWithRawResponse, + AsyncEmploymentsWithRawResponse, +) +from .individuals import ( + Individuals, + AsyncIndividuals, + IndividualsWithRawResponse, + AsyncIndividualsWithRawResponse, +) +from .pay_statements import ( + PayStatements, + AsyncPayStatements, + PayStatementsWithRawResponse, + AsyncPayStatementsWithRawResponse, +) if TYPE_CHECKING: from ..._client import Finch, AsyncFinch @@ -27,6 +62,7 @@ class HRIS(SyncAPIResource): payments: Payments pay_statements: PayStatements benefits: Benefits + with_raw_response: HRISWithRawResponse def __init__(self, client: Finch) -> None: super().__init__(client) @@ -37,6 +73,7 @@ def __init__(self, client: Finch) -> None: self.payments = Payments(client) self.pay_statements = PayStatements(client) self.benefits = Benefits(client) + self.with_raw_response = HRISWithRawResponse(self) class AsyncHRIS(AsyncAPIResource): @@ -47,6 +84,7 @@ class AsyncHRIS(AsyncAPIResource): payments: AsyncPayments pay_statements: AsyncPayStatements benefits: AsyncBenefits + with_raw_response: AsyncHRISWithRawResponse def __init__(self, client: AsyncFinch) -> None: super().__init__(client) @@ -57,3 +95,26 @@ def __init__(self, client: AsyncFinch) -> None: self.payments = AsyncPayments(client) self.pay_statements = AsyncPayStatements(client) self.benefits = AsyncBenefits(client) + self.with_raw_response = AsyncHRISWithRawResponse(self) + + +class HRISWithRawResponse: + def __init__(self, hris: HRIS) -> None: + self.company = CompanyResourceWithRawResponse(hris.company) + self.directory = DirectoryWithRawResponse(hris.directory) + self.individuals = IndividualsWithRawResponse(hris.individuals) + self.employments = EmploymentsWithRawResponse(hris.employments) + self.payments = PaymentsWithRawResponse(hris.payments) + self.pay_statements = PayStatementsWithRawResponse(hris.pay_statements) + self.benefits = BenefitsWithRawResponse(hris.benefits) + + +class AsyncHRISWithRawResponse: + def __init__(self, hris: AsyncHRIS) -> None: + self.company = AsyncCompanyResourceWithRawResponse(hris.company) + self.directory = AsyncDirectoryWithRawResponse(hris.directory) + self.individuals = AsyncIndividualsWithRawResponse(hris.individuals) + self.employments = AsyncEmploymentsWithRawResponse(hris.employments) + self.payments = AsyncPaymentsWithRawResponse(hris.payments) + self.pay_statements = AsyncPayStatementsWithRawResponse(hris.pay_statements) + self.benefits = AsyncBenefitsWithRawResponse(hris.benefits) diff --git a/src/finch/resources/hris/individuals.py b/src/finch/resources/hris/individuals.py index 847cce8a..34622c26 100644 --- a/src/finch/resources/hris/individuals.py +++ b/src/finch/resources/hris/individuals.py @@ -2,19 +2,29 @@ from __future__ import annotations -from typing import List, Optional +from typing import TYPE_CHECKING, List, Optional from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven from ..._utils import maybe_transform from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import to_raw_response_wrapper, async_to_raw_response_wrapper from ...pagination import SyncResponsesPage, AsyncResponsesPage from ...types.hris import IndividualResponse, individual_retrieve_many_params from ..._base_client import AsyncPaginator, make_request_options +if TYPE_CHECKING: + from ..._client import Finch, AsyncFinch + __all__ = ["Individuals", "AsyncIndividuals"] class Individuals(SyncAPIResource): + with_raw_response: IndividualsWithRawResponse + + def __init__(self, client: Finch) -> None: + super().__init__(client) + self.with_raw_response = IndividualsWithRawResponse(self) + def retrieve_many( self, *, @@ -58,6 +68,12 @@ def retrieve_many( class AsyncIndividuals(AsyncAPIResource): + with_raw_response: AsyncIndividualsWithRawResponse + + def __init__(self, client: AsyncFinch) -> None: + super().__init__(client) + self.with_raw_response = AsyncIndividualsWithRawResponse(self) + def retrieve_many( self, *, @@ -98,3 +114,17 @@ def retrieve_many( model=IndividualResponse, method="post", ) + + +class IndividualsWithRawResponse: + def __init__(self, individuals: Individuals) -> None: + self.retrieve_many = to_raw_response_wrapper( + individuals.retrieve_many, + ) + + +class AsyncIndividualsWithRawResponse: + def __init__(self, individuals: AsyncIndividuals) -> None: + self.retrieve_many = async_to_raw_response_wrapper( + individuals.retrieve_many, + ) diff --git a/src/finch/resources/hris/pay_statements.py b/src/finch/resources/hris/pay_statements.py index 6a78a424..242cb846 100644 --- a/src/finch/resources/hris/pay_statements.py +++ b/src/finch/resources/hris/pay_statements.py @@ -2,19 +2,29 @@ from __future__ import annotations -from typing import List +from typing import TYPE_CHECKING, List from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven from ..._utils import maybe_transform from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import to_raw_response_wrapper, async_to_raw_response_wrapper from ...pagination import SyncResponsesPage, AsyncResponsesPage from ...types.hris import PayStatementResponse, pay_statement_retrieve_many_params from ..._base_client import AsyncPaginator, make_request_options +if TYPE_CHECKING: + from ..._client import Finch, AsyncFinch + __all__ = ["PayStatements", "AsyncPayStatements"] class PayStatements(SyncAPIResource): + with_raw_response: PayStatementsWithRawResponse + + def __init__(self, client: Finch) -> None: + super().__init__(client) + self.with_raw_response = PayStatementsWithRawResponse(self) + def retrieve_many( self, *, @@ -58,6 +68,12 @@ def retrieve_many( class AsyncPayStatements(AsyncAPIResource): + with_raw_response: AsyncPayStatementsWithRawResponse + + def __init__(self, client: AsyncFinch) -> None: + super().__init__(client) + self.with_raw_response = AsyncPayStatementsWithRawResponse(self) + def retrieve_many( self, *, @@ -98,3 +114,17 @@ def retrieve_many( model=PayStatementResponse, method="post", ) + + +class PayStatementsWithRawResponse: + def __init__(self, pay_statements: PayStatements) -> None: + self.retrieve_many = to_raw_response_wrapper( + pay_statements.retrieve_many, + ) + + +class AsyncPayStatementsWithRawResponse: + def __init__(self, pay_statements: AsyncPayStatements) -> None: + self.retrieve_many = async_to_raw_response_wrapper( + pay_statements.retrieve_many, + ) diff --git a/src/finch/resources/hris/payments.py b/src/finch/resources/hris/payments.py index 15856203..8a6a0e17 100644 --- a/src/finch/resources/hris/payments.py +++ b/src/finch/resources/hris/payments.py @@ -2,20 +2,30 @@ from __future__ import annotations -from typing import Union +from typing import TYPE_CHECKING, Union from datetime import date from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven from ..._utils import maybe_transform from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import to_raw_response_wrapper, async_to_raw_response_wrapper from ...pagination import SyncSinglePage, AsyncSinglePage from ...types.hris import Payment, payment_list_params from ..._base_client import AsyncPaginator, make_request_options +if TYPE_CHECKING: + from ..._client import Finch, AsyncFinch + __all__ = ["Payments", "AsyncPayments"] class Payments(SyncAPIResource): + with_raw_response: PaymentsWithRawResponse + + def __init__(self, client: Finch) -> None: + super().__init__(client) + self.with_raw_response = PaymentsWithRawResponse(self) + def list( self, *, @@ -67,6 +77,12 @@ def list( class AsyncPayments(AsyncAPIResource): + with_raw_response: AsyncPaymentsWithRawResponse + + def __init__(self, client: AsyncFinch) -> None: + super().__init__(client) + self.with_raw_response = AsyncPaymentsWithRawResponse(self) + def list( self, *, @@ -115,3 +131,17 @@ def list( ), model=Payment, ) + + +class PaymentsWithRawResponse: + def __init__(self, payments: Payments) -> None: + self.list = to_raw_response_wrapper( + payments.list, + ) + + +class AsyncPaymentsWithRawResponse: + def __init__(self, payments: AsyncPayments) -> None: + self.list = async_to_raw_response_wrapper( + payments.list, + ) diff --git a/src/finch/resources/providers.py b/src/finch/resources/providers.py index 57d70a3b..1dbea42b 100644 --- a/src/finch/resources/providers.py +++ b/src/finch/resources/providers.py @@ -2,16 +2,28 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from ..types import Provider from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import to_raw_response_wrapper, async_to_raw_response_wrapper from ..pagination import SyncSinglePage, AsyncSinglePage from .._base_client import AsyncPaginator, make_request_options +if TYPE_CHECKING: + from .._client import Finch, AsyncFinch + __all__ = ["Providers", "AsyncProviders"] class Providers(SyncAPIResource): + with_raw_response: ProvidersWithRawResponse + + def __init__(self, client: Finch) -> None: + super().__init__(client) + self.with_raw_response = ProvidersWithRawResponse(self) + def list( self, *, @@ -34,6 +46,12 @@ def list( class AsyncProviders(AsyncAPIResource): + with_raw_response: AsyncProvidersWithRawResponse + + def __init__(self, client: AsyncFinch) -> None: + super().__init__(client) + self.with_raw_response = AsyncProvidersWithRawResponse(self) + def list( self, *, @@ -53,3 +71,17 @@ def list( ), model=Provider, ) + + +class ProvidersWithRawResponse: + def __init__(self, providers: Providers) -> None: + self.list = to_raw_response_wrapper( + providers.list, + ) + + +class AsyncProvidersWithRawResponse: + def __init__(self, providers: AsyncProviders) -> None: + self.list = async_to_raw_response_wrapper( + providers.list, + ) diff --git a/src/finch/resources/request_forwarding.py b/src/finch/resources/request_forwarding.py index 715eba54..073654ed 100644 --- a/src/finch/resources/request_forwarding.py +++ b/src/finch/resources/request_forwarding.py @@ -2,18 +2,28 @@ from __future__ import annotations -from typing import Optional +from typing import TYPE_CHECKING, Optional from ..types import RequestForwardingForwardResponse, request_forwarding_forward_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven from .._utils import maybe_transform from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import to_raw_response_wrapper, async_to_raw_response_wrapper from .._base_client import make_request_options +if TYPE_CHECKING: + from .._client import Finch, AsyncFinch + __all__ = ["RequestForwarding", "AsyncRequestForwarding"] class RequestForwarding(SyncAPIResource): + with_raw_response: RequestForwardingWithRawResponse + + def __init__(self, client: Finch) -> None: + super().__init__(client) + self.with_raw_response = RequestForwardingWithRawResponse(self) + def forward( self, *, @@ -78,6 +88,12 @@ def forward( class AsyncRequestForwarding(AsyncAPIResource): + with_raw_response: AsyncRequestForwardingWithRawResponse + + def __init__(self, client: AsyncFinch) -> None: + super().__init__(client) + self.with_raw_response = AsyncRequestForwardingWithRawResponse(self) + async def forward( self, *, @@ -139,3 +155,17 @@ async def forward( ), cast_to=RequestForwardingForwardResponse, ) + + +class RequestForwardingWithRawResponse: + def __init__(self, request_forwarding: RequestForwarding) -> None: + self.forward = to_raw_response_wrapper( + request_forwarding.forward, + ) + + +class AsyncRequestForwardingWithRawResponse: + def __init__(self, request_forwarding: AsyncRequestForwarding) -> None: + self.forward = async_to_raw_response_wrapper( + request_forwarding.forward, + ) diff --git a/src/finch/resources/webhooks.py b/src/finch/resources/webhooks.py index 2948150e..13b88d61 100644 --- a/src/finch/resources/webhooks.py +++ b/src/finch/resources/webhooks.py @@ -7,16 +7,23 @@ import math import base64 import hashlib +from typing import TYPE_CHECKING from datetime import datetime, timezone, timedelta from .._types import HeadersLike from .._utils import get_required_header from .._resource import SyncAPIResource, AsyncAPIResource +if TYPE_CHECKING: + from .._client import Finch, AsyncFinch + __all__ = ["Webhooks", "AsyncWebhooks"] class Webhooks(SyncAPIResource): + def __init__(self, client: Finch) -> None: + super().__init__(client) + def unwrap( self, payload: str | bytes, @@ -110,6 +117,9 @@ def verify_signature( class AsyncWebhooks(AsyncAPIResource): + def __init__(self, client: AsyncFinch) -> None: + super().__init__(client) + def unwrap( self, payload: str | bytes, diff --git a/tests/api_resources/hris/benefits/test_individuals.py b/tests/api_resources/hris/benefits/test_individuals.py index 2d37e28f..3c45aa12 100644 --- a/tests/api_resources/hris/benefits/test_individuals.py +++ b/tests/api_resources/hris/benefits/test_individuals.py @@ -8,6 +8,7 @@ from finch import Finch, AsyncFinch from tests.utils import assert_matches_type +from finch._client import Finch, AsyncFinch from finch.pagination import SyncSinglePage, AsyncSinglePage from finch.types.hris.benefits import ( IndividualBenefit, @@ -33,6 +34,16 @@ def test_method_enroll_many(self, client: Finch) -> None: ) assert_matches_type(SyncSinglePage[EnrolledIndividual], individual, path=["response"]) + @parametrize + def test_raw_response_enroll_many(self, client: Finch) -> None: + response = client.hris.benefits.individuals.with_raw_response.enroll_many( + "string", + individuals=[{}], + ) + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + individual = response.parse() + assert_matches_type(SyncSinglePage[EnrolledIndividual], individual, path=["response"]) + @parametrize def test_method_enrolled_ids(self, client: Finch) -> None: individual = client.hris.benefits.individuals.enrolled_ids( @@ -40,6 +51,15 @@ def test_method_enrolled_ids(self, client: Finch) -> None: ) assert_matches_type(IndividualEnrolledIDsResponse, individual, path=["response"]) + @parametrize + def test_raw_response_enrolled_ids(self, client: Finch) -> None: + response = client.hris.benefits.individuals.with_raw_response.enrolled_ids( + "string", + ) + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + individual = response.parse() + assert_matches_type(IndividualEnrolledIDsResponse, individual, path=["response"]) + @parametrize def test_method_retrieve_many_benefits(self, client: Finch) -> None: individual = client.hris.benefits.individuals.retrieve_many_benefits( @@ -55,6 +75,15 @@ def test_method_retrieve_many_benefits_with_all_params(self, client: Finch) -> N ) assert_matches_type(SyncSinglePage[IndividualBenefit], individual, path=["response"]) + @parametrize + def test_raw_response_retrieve_many_benefits(self, client: Finch) -> None: + response = client.hris.benefits.individuals.with_raw_response.retrieve_many_benefits( + "string", + ) + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + individual = response.parse() + assert_matches_type(SyncSinglePage[IndividualBenefit], individual, path=["response"]) + @parametrize def test_method_unenroll_many(self, client: Finch) -> None: individual = client.hris.benefits.individuals.unenroll_many( @@ -70,6 +99,15 @@ def test_method_unenroll_many_with_all_params(self, client: Finch) -> None: ) assert_matches_type(SyncSinglePage[UnenrolledIndividual], individual, path=["response"]) + @parametrize + def test_raw_response_unenroll_many(self, client: Finch) -> None: + response = client.hris.benefits.individuals.with_raw_response.unenroll_many( + "string", + ) + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + individual = response.parse() + assert_matches_type(SyncSinglePage[UnenrolledIndividual], individual, path=["response"]) + class TestAsyncIndividuals: strict_client = AsyncFinch(base_url=base_url, access_token=access_token, _strict_response_validation=True) @@ -84,6 +122,16 @@ async def test_method_enroll_many(self, client: AsyncFinch) -> None: ) assert_matches_type(AsyncSinglePage[EnrolledIndividual], individual, path=["response"]) + @parametrize + async def test_raw_response_enroll_many(self, client: AsyncFinch) -> None: + response = await client.hris.benefits.individuals.with_raw_response.enroll_many( + "string", + individuals=[{}], + ) + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + individual = response.parse() + assert_matches_type(AsyncSinglePage[EnrolledIndividual], individual, path=["response"]) + @parametrize async def test_method_enrolled_ids(self, client: AsyncFinch) -> None: individual = await client.hris.benefits.individuals.enrolled_ids( @@ -91,6 +139,15 @@ async def test_method_enrolled_ids(self, client: AsyncFinch) -> None: ) assert_matches_type(IndividualEnrolledIDsResponse, individual, path=["response"]) + @parametrize + async def test_raw_response_enrolled_ids(self, client: AsyncFinch) -> None: + response = await client.hris.benefits.individuals.with_raw_response.enrolled_ids( + "string", + ) + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + individual = response.parse() + assert_matches_type(IndividualEnrolledIDsResponse, individual, path=["response"]) + @parametrize async def test_method_retrieve_many_benefits(self, client: AsyncFinch) -> None: individual = await client.hris.benefits.individuals.retrieve_many_benefits( @@ -106,6 +163,15 @@ async def test_method_retrieve_many_benefits_with_all_params(self, client: Async ) assert_matches_type(AsyncSinglePage[IndividualBenefit], individual, path=["response"]) + @parametrize + async def test_raw_response_retrieve_many_benefits(self, client: AsyncFinch) -> None: + response = await client.hris.benefits.individuals.with_raw_response.retrieve_many_benefits( + "string", + ) + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + individual = response.parse() + assert_matches_type(AsyncSinglePage[IndividualBenefit], individual, path=["response"]) + @parametrize async def test_method_unenroll_many(self, client: AsyncFinch) -> None: individual = await client.hris.benefits.individuals.unenroll_many( @@ -120,3 +186,12 @@ async def test_method_unenroll_many_with_all_params(self, client: AsyncFinch) -> individual_ids=["string", "string", "string"], ) assert_matches_type(AsyncSinglePage[UnenrolledIndividual], individual, path=["response"]) + + @parametrize + async def test_raw_response_unenroll_many(self, client: AsyncFinch) -> None: + response = await client.hris.benefits.individuals.with_raw_response.unenroll_many( + "string", + ) + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + individual = response.parse() + assert_matches_type(AsyncSinglePage[UnenrolledIndividual], individual, path=["response"]) diff --git a/tests/api_resources/hris/test_benefits.py b/tests/api_resources/hris/test_benefits.py index 3d464f66..77f4caff 100644 --- a/tests/api_resources/hris/test_benefits.py +++ b/tests/api_resources/hris/test_benefits.py @@ -8,6 +8,7 @@ from finch import Finch, AsyncFinch from tests.utils import assert_matches_type +from finch._client import Finch, AsyncFinch from finch.pagination import SyncSinglePage, AsyncSinglePage from finch.types.hris import ( CompanyBenefit, @@ -39,6 +40,13 @@ def test_method_create_with_all_params(self, client: Finch) -> None: ) assert_matches_type(CreateCompanyBenefitsResponse, benefit, path=["response"]) + @parametrize + def test_raw_response_create(self, client: Finch) -> None: + response = client.hris.benefits.with_raw_response.create() + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benefit = response.parse() + assert_matches_type(CreateCompanyBenefitsResponse, benefit, path=["response"]) + @parametrize def test_method_retrieve(self, client: Finch) -> None: benefit = client.hris.benefits.retrieve( @@ -46,6 +54,15 @@ def test_method_retrieve(self, client: Finch) -> None: ) assert_matches_type(CompanyBenefit, benefit, path=["response"]) + @parametrize + def test_raw_response_retrieve(self, client: Finch) -> None: + response = client.hris.benefits.with_raw_response.retrieve( + "string", + ) + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benefit = response.parse() + assert_matches_type(CompanyBenefit, benefit, path=["response"]) + @parametrize def test_method_update(self, client: Finch) -> None: benefit = client.hris.benefits.update( @@ -61,16 +78,39 @@ def test_method_update_with_all_params(self, client: Finch) -> None: ) assert_matches_type(UpdateCompanyBenefitResponse, benefit, path=["response"]) + @parametrize + def test_raw_response_update(self, client: Finch) -> None: + response = client.hris.benefits.with_raw_response.update( + "string", + ) + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benefit = response.parse() + assert_matches_type(UpdateCompanyBenefitResponse, benefit, path=["response"]) + @parametrize def test_method_list(self, client: Finch) -> None: benefit = client.hris.benefits.list() assert_matches_type(SyncSinglePage[CompanyBenefit], benefit, path=["response"]) + @parametrize + def test_raw_response_list(self, client: Finch) -> None: + response = client.hris.benefits.with_raw_response.list() + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benefit = response.parse() + assert_matches_type(SyncSinglePage[CompanyBenefit], benefit, path=["response"]) + @parametrize def test_method_list_supported_benefits(self, client: Finch) -> None: benefit = client.hris.benefits.list_supported_benefits() assert_matches_type(SyncSinglePage[SupportedBenefit], benefit, path=["response"]) + @parametrize + def test_raw_response_list_supported_benefits(self, client: Finch) -> None: + response = client.hris.benefits.with_raw_response.list_supported_benefits() + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benefit = response.parse() + assert_matches_type(SyncSinglePage[SupportedBenefit], benefit, path=["response"]) + class TestAsyncBenefits: strict_client = AsyncFinch(base_url=base_url, access_token=access_token, _strict_response_validation=True) @@ -91,6 +131,13 @@ async def test_method_create_with_all_params(self, client: AsyncFinch) -> None: ) assert_matches_type(CreateCompanyBenefitsResponse, benefit, path=["response"]) + @parametrize + async def test_raw_response_create(self, client: AsyncFinch) -> None: + response = await client.hris.benefits.with_raw_response.create() + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benefit = response.parse() + assert_matches_type(CreateCompanyBenefitsResponse, benefit, path=["response"]) + @parametrize async def test_method_retrieve(self, client: AsyncFinch) -> None: benefit = await client.hris.benefits.retrieve( @@ -98,6 +145,15 @@ async def test_method_retrieve(self, client: AsyncFinch) -> None: ) assert_matches_type(CompanyBenefit, benefit, path=["response"]) + @parametrize + async def test_raw_response_retrieve(self, client: AsyncFinch) -> None: + response = await client.hris.benefits.with_raw_response.retrieve( + "string", + ) + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benefit = response.parse() + assert_matches_type(CompanyBenefit, benefit, path=["response"]) + @parametrize async def test_method_update(self, client: AsyncFinch) -> None: benefit = await client.hris.benefits.update( @@ -113,12 +169,35 @@ async def test_method_update_with_all_params(self, client: AsyncFinch) -> None: ) assert_matches_type(UpdateCompanyBenefitResponse, benefit, path=["response"]) + @parametrize + async def test_raw_response_update(self, client: AsyncFinch) -> None: + response = await client.hris.benefits.with_raw_response.update( + "string", + ) + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benefit = response.parse() + assert_matches_type(UpdateCompanyBenefitResponse, benefit, path=["response"]) + @parametrize async def test_method_list(self, client: AsyncFinch) -> None: benefit = await client.hris.benefits.list() assert_matches_type(AsyncSinglePage[CompanyBenefit], benefit, path=["response"]) + @parametrize + async def test_raw_response_list(self, client: AsyncFinch) -> None: + response = await client.hris.benefits.with_raw_response.list() + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benefit = response.parse() + assert_matches_type(AsyncSinglePage[CompanyBenefit], benefit, path=["response"]) + @parametrize async def test_method_list_supported_benefits(self, client: AsyncFinch) -> None: benefit = await client.hris.benefits.list_supported_benefits() assert_matches_type(AsyncSinglePage[SupportedBenefit], benefit, path=["response"]) + + @parametrize + async def test_raw_response_list_supported_benefits(self, client: AsyncFinch) -> None: + response = await client.hris.benefits.with_raw_response.list_supported_benefits() + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + benefit = response.parse() + assert_matches_type(AsyncSinglePage[SupportedBenefit], benefit, path=["response"]) diff --git a/tests/api_resources/hris/test_company.py b/tests/api_resources/hris/test_company.py index 9ded6c88..2f51d84f 100644 --- a/tests/api_resources/hris/test_company.py +++ b/tests/api_resources/hris/test_company.py @@ -8,6 +8,7 @@ from finch import Finch, AsyncFinch from tests.utils import assert_matches_type +from finch._client import Finch, AsyncFinch from finch.types.hris import Company base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -24,6 +25,13 @@ def test_method_retrieve(self, client: Finch) -> None: company = client.hris.company.retrieve() assert_matches_type(Company, company, path=["response"]) + @parametrize + def test_raw_response_retrieve(self, client: Finch) -> None: + response = client.hris.company.with_raw_response.retrieve() + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + company = response.parse() + assert_matches_type(Company, company, path=["response"]) + class TestAsyncCompany: strict_client = AsyncFinch(base_url=base_url, access_token=access_token, _strict_response_validation=True) @@ -34,3 +42,10 @@ class TestAsyncCompany: async def test_method_retrieve(self, client: AsyncFinch) -> None: company = await client.hris.company.retrieve() assert_matches_type(Company, company, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, client: AsyncFinch) -> None: + response = await client.hris.company.with_raw_response.retrieve() + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + company = response.parse() + assert_matches_type(Company, company, path=["response"]) diff --git a/tests/api_resources/hris/test_directory.py b/tests/api_resources/hris/test_directory.py index 5b4b3ff6..53dd541d 100644 --- a/tests/api_resources/hris/test_directory.py +++ b/tests/api_resources/hris/test_directory.py @@ -8,9 +8,12 @@ from finch import Finch, AsyncFinch from tests.utils import assert_matches_type +from finch._client import Finch, AsyncFinch from finch.pagination import SyncIndividualsPage, AsyncIndividualsPage from finch.types.hris import IndividualInDirectory +# pyright: reportDeprecated=false + base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") access_token = "My Access Token" @@ -33,21 +36,36 @@ def test_method_list_with_all_params(self, client: Finch) -> None: ) assert_matches_type(SyncIndividualsPage[IndividualInDirectory], directory, path=["response"]) + @parametrize + def test_raw_response_list(self, client: Finch) -> None: + response = client.hris.directory.with_raw_response.list() + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + directory = response.parse() + assert_matches_type(SyncIndividualsPage[IndividualInDirectory], directory, path=["response"]) + @parametrize def test_method_list_individuals(self, client: Finch) -> None: with pytest.warns(DeprecationWarning): - directory = client.hris.directory.list_individuals() # pyright: ignore[reportDeprecated] + directory = client.hris.directory.list_individuals() assert_matches_type(SyncIndividualsPage[IndividualInDirectory], directory, path=["response"]) @parametrize def test_method_list_individuals_with_all_params(self, client: Finch) -> None: with pytest.warns(DeprecationWarning): - directory = client.hris.directory.list_individuals( # pyright: ignore[reportDeprecated] + directory = client.hris.directory.list_individuals( limit=0, offset=0, ) assert_matches_type(SyncIndividualsPage[IndividualInDirectory], directory, path=["response"]) + @parametrize + def test_raw_response_list_individuals(self, client: Finch) -> None: + with pytest.warns(DeprecationWarning): + response = client.hris.directory.with_raw_response.list_individuals() + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + directory = response.parse() + assert_matches_type(SyncIndividualsPage[IndividualInDirectory], directory, path=["response"]) + class TestAsyncDirectory: strict_client = AsyncFinch(base_url=base_url, access_token=access_token, _strict_response_validation=True) @@ -67,17 +85,32 @@ async def test_method_list_with_all_params(self, client: AsyncFinch) -> None: ) assert_matches_type(AsyncIndividualsPage[IndividualInDirectory], directory, path=["response"]) + @parametrize + async def test_raw_response_list(self, client: AsyncFinch) -> None: + response = await client.hris.directory.with_raw_response.list() + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + directory = response.parse() + assert_matches_type(AsyncIndividualsPage[IndividualInDirectory], directory, path=["response"]) + @parametrize async def test_method_list_individuals(self, client: AsyncFinch) -> None: with pytest.warns(DeprecationWarning): - directory = await client.hris.directory.list_individuals() # pyright: ignore[reportDeprecated] + directory = await client.hris.directory.list_individuals() assert_matches_type(AsyncIndividualsPage[IndividualInDirectory], directory, path=["response"]) @parametrize async def test_method_list_individuals_with_all_params(self, client: AsyncFinch) -> None: with pytest.warns(DeprecationWarning): - directory = await client.hris.directory.list_individuals( # pyright: ignore[reportDeprecated] + directory = await client.hris.directory.list_individuals( limit=0, offset=0, ) assert_matches_type(AsyncIndividualsPage[IndividualInDirectory], directory, path=["response"]) + + @parametrize + async def test_raw_response_list_individuals(self, client: AsyncFinch) -> None: + with pytest.warns(DeprecationWarning): + response = await client.hris.directory.with_raw_response.list_individuals() + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + directory = response.parse() + assert_matches_type(AsyncIndividualsPage[IndividualInDirectory], directory, path=["response"]) diff --git a/tests/api_resources/hris/test_employments.py b/tests/api_resources/hris/test_employments.py index b86990aa..fd6d2499 100644 --- a/tests/api_resources/hris/test_employments.py +++ b/tests/api_resources/hris/test_employments.py @@ -8,6 +8,7 @@ from finch import Finch, AsyncFinch from tests.utils import assert_matches_type +from finch._client import Finch, AsyncFinch from finch.pagination import SyncResponsesPage, AsyncResponsesPage from finch.types.hris import EmploymentDataResponse @@ -27,6 +28,15 @@ def test_method_retrieve_many(self, client: Finch) -> None: ) assert_matches_type(SyncResponsesPage[EmploymentDataResponse], employment, path=["response"]) + @parametrize + def test_raw_response_retrieve_many(self, client: Finch) -> None: + response = client.hris.employments.with_raw_response.retrieve_many( + requests=[{"individual_id": "string"}, {"individual_id": "string"}, {"individual_id": "string"}], + ) + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employment = response.parse() + assert_matches_type(SyncResponsesPage[EmploymentDataResponse], employment, path=["response"]) + class TestAsyncEmployments: strict_client = AsyncFinch(base_url=base_url, access_token=access_token, _strict_response_validation=True) @@ -39,3 +49,12 @@ async def test_method_retrieve_many(self, client: AsyncFinch) -> None: requests=[{"individual_id": "string"}, {"individual_id": "string"}, {"individual_id": "string"}], ) assert_matches_type(AsyncResponsesPage[EmploymentDataResponse], employment, path=["response"]) + + @parametrize + async def test_raw_response_retrieve_many(self, client: AsyncFinch) -> None: + response = await client.hris.employments.with_raw_response.retrieve_many( + requests=[{"individual_id": "string"}, {"individual_id": "string"}, {"individual_id": "string"}], + ) + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employment = response.parse() + assert_matches_type(AsyncResponsesPage[EmploymentDataResponse], employment, path=["response"]) diff --git a/tests/api_resources/hris/test_individuals.py b/tests/api_resources/hris/test_individuals.py index 6c910950..d109dbe9 100644 --- a/tests/api_resources/hris/test_individuals.py +++ b/tests/api_resources/hris/test_individuals.py @@ -8,6 +8,7 @@ from finch import Finch, AsyncFinch from tests.utils import assert_matches_type +from finch._client import Finch, AsyncFinch from finch.pagination import SyncResponsesPage, AsyncResponsesPage from finch.types.hris import IndividualResponse @@ -33,6 +34,13 @@ def test_method_retrieve_many_with_all_params(self, client: Finch) -> None: ) assert_matches_type(SyncResponsesPage[IndividualResponse], individual, path=["response"]) + @parametrize + def test_raw_response_retrieve_many(self, client: Finch) -> None: + response = client.hris.individuals.with_raw_response.retrieve_many() + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + individual = response.parse() + assert_matches_type(SyncResponsesPage[IndividualResponse], individual, path=["response"]) + class TestAsyncIndividuals: strict_client = AsyncFinch(base_url=base_url, access_token=access_token, _strict_response_validation=True) @@ -51,3 +59,10 @@ async def test_method_retrieve_many_with_all_params(self, client: AsyncFinch) -> requests=[{"individual_id": "string"}, {"individual_id": "string"}, {"individual_id": "string"}], ) assert_matches_type(AsyncResponsesPage[IndividualResponse], individual, path=["response"]) + + @parametrize + async def test_raw_response_retrieve_many(self, client: AsyncFinch) -> None: + response = await client.hris.individuals.with_raw_response.retrieve_many() + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + individual = response.parse() + assert_matches_type(AsyncResponsesPage[IndividualResponse], individual, path=["response"]) diff --git a/tests/api_resources/hris/test_pay_statements.py b/tests/api_resources/hris/test_pay_statements.py index cc536268..bc6b3d39 100644 --- a/tests/api_resources/hris/test_pay_statements.py +++ b/tests/api_resources/hris/test_pay_statements.py @@ -8,6 +8,7 @@ from finch import Finch, AsyncFinch from tests.utils import assert_matches_type +from finch._client import Finch, AsyncFinch from finch.pagination import SyncResponsesPage, AsyncResponsesPage from finch.types.hris import PayStatementResponse @@ -31,6 +32,19 @@ def test_method_retrieve_many(self, client: Finch) -> None: ) assert_matches_type(SyncResponsesPage[PayStatementResponse], pay_statement, path=["response"]) + @parametrize + def test_raw_response_retrieve_many(self, client: Finch) -> None: + response = client.hris.pay_statements.with_raw_response.retrieve_many( + requests=[ + {"payment_id": "e8b90071-0c11-471c-86e8-e303ef2f6782"}, + {"payment_id": "e8b90071-0c11-471c-86e8-e303ef2f6782"}, + {"payment_id": "e8b90071-0c11-471c-86e8-e303ef2f6782"}, + ], + ) + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + pay_statement = response.parse() + assert_matches_type(SyncResponsesPage[PayStatementResponse], pay_statement, path=["response"]) + class TestAsyncPayStatements: strict_client = AsyncFinch(base_url=base_url, access_token=access_token, _strict_response_validation=True) @@ -47,3 +61,16 @@ async def test_method_retrieve_many(self, client: AsyncFinch) -> None: ], ) assert_matches_type(AsyncResponsesPage[PayStatementResponse], pay_statement, path=["response"]) + + @parametrize + async def test_raw_response_retrieve_many(self, client: AsyncFinch) -> None: + response = await client.hris.pay_statements.with_raw_response.retrieve_many( + requests=[ + {"payment_id": "e8b90071-0c11-471c-86e8-e303ef2f6782"}, + {"payment_id": "e8b90071-0c11-471c-86e8-e303ef2f6782"}, + {"payment_id": "e8b90071-0c11-471c-86e8-e303ef2f6782"}, + ], + ) + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + pay_statement = response.parse() + assert_matches_type(AsyncResponsesPage[PayStatementResponse], pay_statement, path=["response"]) diff --git a/tests/api_resources/hris/test_payments.py b/tests/api_resources/hris/test_payments.py index 7680f63f..b117122b 100644 --- a/tests/api_resources/hris/test_payments.py +++ b/tests/api_resources/hris/test_payments.py @@ -9,6 +9,7 @@ from finch import Finch, AsyncFinch from tests.utils import assert_matches_type from finch._utils import parse_date +from finch._client import Finch, AsyncFinch from finch.pagination import SyncSinglePage, AsyncSinglePage from finch.types.hris import Payment @@ -29,6 +30,16 @@ def test_method_list(self, client: Finch) -> None: ) assert_matches_type(SyncSinglePage[Payment], payment, path=["response"]) + @parametrize + def test_raw_response_list(self, client: Finch) -> None: + response = client.hris.payments.with_raw_response.list( + end_date=parse_date("2021-01-01"), + start_date=parse_date("2021-01-01"), + ) + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + payment = response.parse() + assert_matches_type(SyncSinglePage[Payment], payment, path=["response"]) + class TestAsyncPayments: strict_client = AsyncFinch(base_url=base_url, access_token=access_token, _strict_response_validation=True) @@ -42,3 +53,13 @@ async def test_method_list(self, client: AsyncFinch) -> None: start_date=parse_date("2021-01-01"), ) assert_matches_type(AsyncSinglePage[Payment], payment, path=["response"]) + + @parametrize + async def test_raw_response_list(self, client: AsyncFinch) -> None: + response = await client.hris.payments.with_raw_response.list( + end_date=parse_date("2021-01-01"), + start_date=parse_date("2021-01-01"), + ) + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + payment = response.parse() + assert_matches_type(AsyncSinglePage[Payment], payment, path=["response"]) diff --git a/tests/api_resources/test_account.py b/tests/api_resources/test_account.py index 246afef3..33933b69 100644 --- a/tests/api_resources/test_account.py +++ b/tests/api_resources/test_account.py @@ -9,6 +9,7 @@ from finch import Finch, AsyncFinch from finch.types import Introspection, DisconnectResponse from tests.utils import assert_matches_type +from finch._client import Finch, AsyncFinch base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") access_token = "My Access Token" @@ -24,11 +25,25 @@ def test_method_disconnect(self, client: Finch) -> None: account = client.account.disconnect() assert_matches_type(DisconnectResponse, account, path=["response"]) + @parametrize + def test_raw_response_disconnect(self, client: Finch) -> None: + response = client.account.with_raw_response.disconnect() + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + account = response.parse() + assert_matches_type(DisconnectResponse, account, path=["response"]) + @parametrize def test_method_introspect(self, client: Finch) -> None: account = client.account.introspect() assert_matches_type(Introspection, account, path=["response"]) + @parametrize + def test_raw_response_introspect(self, client: Finch) -> None: + response = client.account.with_raw_response.introspect() + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + account = response.parse() + assert_matches_type(Introspection, account, path=["response"]) + class TestAsyncAccount: strict_client = AsyncFinch(base_url=base_url, access_token=access_token, _strict_response_validation=True) @@ -40,7 +55,21 @@ async def test_method_disconnect(self, client: AsyncFinch) -> None: account = await client.account.disconnect() assert_matches_type(DisconnectResponse, account, path=["response"]) + @parametrize + async def test_raw_response_disconnect(self, client: AsyncFinch) -> None: + response = await client.account.with_raw_response.disconnect() + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + account = response.parse() + assert_matches_type(DisconnectResponse, account, path=["response"]) + @parametrize async def test_method_introspect(self, client: AsyncFinch) -> None: account = await client.account.introspect() assert_matches_type(Introspection, account, path=["response"]) + + @parametrize + async def test_raw_response_introspect(self, client: AsyncFinch) -> None: + response = await client.account.with_raw_response.introspect() + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + account = response.parse() + assert_matches_type(Introspection, account, path=["response"]) diff --git a/tests/api_resources/test_providers.py b/tests/api_resources/test_providers.py index 6229d747..bb0803d5 100644 --- a/tests/api_resources/test_providers.py +++ b/tests/api_resources/test_providers.py @@ -9,6 +9,7 @@ from finch import Finch, AsyncFinch from finch.types import Provider from tests.utils import assert_matches_type +from finch._client import Finch, AsyncFinch from finch.pagination import SyncSinglePage, AsyncSinglePage base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -25,6 +26,13 @@ def test_method_list(self, client: Finch) -> None: provider = client.providers.list() assert_matches_type(SyncSinglePage[Provider], provider, path=["response"]) + @parametrize + def test_raw_response_list(self, client: Finch) -> None: + response = client.providers.with_raw_response.list() + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + provider = response.parse() + assert_matches_type(SyncSinglePage[Provider], provider, path=["response"]) + class TestAsyncProviders: strict_client = AsyncFinch(base_url=base_url, access_token=access_token, _strict_response_validation=True) @@ -35,3 +43,10 @@ class TestAsyncProviders: async def test_method_list(self, client: AsyncFinch) -> None: provider = await client.providers.list() assert_matches_type(AsyncSinglePage[Provider], provider, path=["response"]) + + @parametrize + async def test_raw_response_list(self, client: AsyncFinch) -> None: + response = await client.providers.with_raw_response.list() + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + provider = response.parse() + assert_matches_type(AsyncSinglePage[Provider], provider, path=["response"]) diff --git a/tests/api_resources/test_request_forwarding.py b/tests/api_resources/test_request_forwarding.py index 754acda9..66d2dbdf 100644 --- a/tests/api_resources/test_request_forwarding.py +++ b/tests/api_resources/test_request_forwarding.py @@ -9,6 +9,7 @@ from finch import Finch, AsyncFinch from finch.types import RequestForwardingForwardResponse from tests.utils import assert_matches_type +from finch._client import Finch, AsyncFinch base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") access_token = "My Access Token" @@ -41,6 +42,16 @@ def test_method_forward_with_all_params(self, client: Finch) -> None: ) assert_matches_type(RequestForwardingForwardResponse, request_forwarding, path=["response"]) + @parametrize + def test_raw_response_forward(self, client: Finch) -> None: + response = client.request_forwarding.with_raw_response.forward( + method="POST", + route="/people/search", + ) + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + request_forwarding = response.parse() + assert_matches_type(RequestForwardingForwardResponse, request_forwarding, path=["response"]) + class TestAsyncRequestForwarding: strict_client = AsyncFinch(base_url=base_url, access_token=access_token, _strict_response_validation=True) @@ -68,3 +79,13 @@ async def test_method_forward_with_all_params(self, client: AsyncFinch) -> None: }, ) assert_matches_type(RequestForwardingForwardResponse, request_forwarding, path=["response"]) + + @parametrize + async def test_raw_response_forward(self, client: AsyncFinch) -> None: + response = await client.request_forwarding.with_raw_response.forward( + method="POST", + route="/people/search", + ) + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + request_forwarding = response.parse() + assert_matches_type(RequestForwardingForwardResponse, request_forwarding, path=["response"]) diff --git a/tests/api_resources/test_top_level.py b/tests/api_resources/test_top_level.py index 9809a202..a36d997f 100644 --- a/tests/api_resources/test_top_level.py +++ b/tests/api_resources/test_top_level.py @@ -7,6 +7,7 @@ import pytest from finch import Finch, AsyncFinch +from finch._client import Finch, AsyncFinch base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") access_token = "My Access Token" diff --git a/tests/api_resources/test_webhooks.py b/tests/api_resources/test_webhooks.py index 8a0a5ce1..ca6219fb 100644 --- a/tests/api_resources/test_webhooks.py +++ b/tests/api_resources/test_webhooks.py @@ -11,6 +11,7 @@ import time_machine from finch import Finch, AsyncFinch +from finch._client import Finch, AsyncFinch base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") access_token = "My Access Token" diff --git a/tests/test_client.py b/tests/test_client.py index b5acca48..c7766520 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -16,6 +16,7 @@ from finch import Finch, AsyncFinch, APIResponseValidationError from finch._types import Omit +from finch._client import Finch, AsyncFinch from finch._models import BaseModel, FinalRequestOptions from finch._exceptions import APIResponseValidationError from finch._base_client import ( @@ -29,7 +30,7 @@ access_token = "My Access Token" -def _get_params(client: BaseClient[Any]) -> dict[str, str]: +def _get_params(client: BaseClient[Any, Any]) -> dict[str, str]: request = client._build_request(FinalRequestOptions(method="get", url="/foo")) url = httpx.URL(request.url) return dict(url.params) @@ -639,18 +640,20 @@ class Model(BaseModel): "remaining_retries,retry_after,timeout", [ [3, "20", 20], - [3, "0", 2], - [3, "-10", 2], + [3, "0", 0.5], + [3, "-10", 0.5], [3, "60", 60], - [3, "61", 2], + [3, "61", 0.5], [3, "Fri, 29 Sep 2023 16:26:57 GMT", 20], - [3, "Fri, 29 Sep 2023 16:26:37 GMT", 2], - [3, "Fri, 29 Sep 2023 16:26:27 GMT", 2], + [3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5], + [3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5], [3, "Fri, 29 Sep 2023 16:27:37 GMT", 60], - [3, "Fri, 29 Sep 2023 16:27:38 GMT", 2], - [3, "99999999999999999999999999999999999", 2], - [3, "Zun, 29 Sep 2023 16:26:27 GMT", 2], - [3, "", 2], + [3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5], + [3, "99999999999999999999999999999999999", 0.5], + [3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5], + [3, "", 0.5], + [2, "", 0.5 * 2.0], + [1, "", 0.5 * 4.0], ], ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) @@ -658,9 +661,9 @@ def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str client = Finch(base_url=base_url, access_token=access_token, _strict_response_validation=True) headers = httpx.Headers({"retry-after": retry_after}) - options = FinalRequestOptions(method="get", url="/foo", max_retries=2) + options = FinalRequestOptions(method="get", url="/foo", max_retries=3) calculated = client._calculate_retry_timeout(remaining_retries, options, headers) - assert calculated == pytest.approx(timeout, 0.6) # pyright: ignore[reportUnknownMemberType] + assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] class TestAsyncFinch: @@ -1272,18 +1275,20 @@ class Model(BaseModel): "remaining_retries,retry_after,timeout", [ [3, "20", 20], - [3, "0", 2], - [3, "-10", 2], + [3, "0", 0.5], + [3, "-10", 0.5], [3, "60", 60], - [3, "61", 2], + [3, "61", 0.5], [3, "Fri, 29 Sep 2023 16:26:57 GMT", 20], - [3, "Fri, 29 Sep 2023 16:26:37 GMT", 2], - [3, "Fri, 29 Sep 2023 16:26:27 GMT", 2], + [3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5], + [3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5], [3, "Fri, 29 Sep 2023 16:27:37 GMT", 60], - [3, "Fri, 29 Sep 2023 16:27:38 GMT", 2], - [3, "99999999999999999999999999999999999", 2], - [3, "Zun, 29 Sep 2023 16:26:27 GMT", 2], - [3, "", 2], + [3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5], + [3, "99999999999999999999999999999999999", 0.5], + [3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5], + [3, "", 0.5], + [2, "", 0.5 * 2.0], + [1, "", 0.5 * 4.0], ], ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) @@ -1292,6 +1297,6 @@ async def test_parse_retry_after_header(self, remaining_retries: int, retry_afte client = AsyncFinch(base_url=base_url, access_token=access_token, _strict_response_validation=True) headers = httpx.Headers({"retry-after": retry_after}) - options = FinalRequestOptions(method="get", url="/foo", max_retries=2) + options = FinalRequestOptions(method="get", url="/foo", max_retries=3) calculated = client._calculate_retry_timeout(remaining_retries, options, headers) - assert calculated == pytest.approx(timeout, 0.6) # pyright: ignore[reportUnknownMemberType] + assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] diff --git a/tests/test_files.py b/tests/test_files.py new file mode 100644 index 00000000..0ed7d371 --- /dev/null +++ b/tests/test_files.py @@ -0,0 +1,51 @@ +from pathlib import Path + +import anyio +import pytest +from dirty_equals import IsDict, IsList, IsBytes, IsTuple + +from finch._files import to_httpx_files, async_to_httpx_files + +readme_path = Path(__file__).parent.parent.joinpath("README.md") + + +def test_pathlib_includes_file_name() -> None: + result = to_httpx_files({"file": readme_path}) + print(result) + assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) + + +def test_tuple_input() -> None: + result = to_httpx_files([("file", readme_path)]) + print(result) + assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) + + +@pytest.mark.asyncio +async def test_async_pathlib_includes_file_name() -> None: + result = await async_to_httpx_files({"file": readme_path}) + print(result) + assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) + + +@pytest.mark.asyncio +async def test_async_supports_anyio_path() -> None: + result = await async_to_httpx_files({"file": anyio.Path(readme_path)}) + print(result) + assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) + + +@pytest.mark.asyncio +async def test_async_tuple_input() -> None: + result = await async_to_httpx_files([("file", readme_path)]) + print(result) + assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) + + +def test_string_not_allowed() -> None: + with pytest.raises(TypeError, match="Expected file types input to be a FileContent type or to be a tuple"): + to_httpx_files( + { + "file": "foo", # type: ignore + } + )