-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
869b3b4
commit c8d58b0
Showing
62 changed files
with
3,219 additions
and
800 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
# This file was auto-generated by Fern from our API Definition. | ||
|
||
import typing | ||
|
||
# File typing inspired by the flexibility of types within the httpx library | ||
# https://github.com/encode/httpx/blob/master/httpx/_types.py | ||
FileContent = typing.Union[typing.IO[bytes], bytes, str] | ||
File = typing.Union[ | ||
# file (or bytes) | ||
FileContent, | ||
# (filename, file (or bytes)) | ||
typing.Tuple[typing.Optional[str], FileContent], | ||
# (filename, file (or bytes), content_type) | ||
typing.Tuple[typing.Optional[str], FileContent, typing.Optional[str]], | ||
# (filename, file (or bytes), content_type, headers) | ||
typing.Tuple[typing.Optional[str], FileContent, typing.Optional[str], typing.Mapping[str, str]], | ||
] | ||
|
||
|
||
def convert_file_dict_to_httpx_tuples( | ||
d: typing.Dict[str, typing.Union[File, typing.List[File]]] | ||
) -> typing.List[typing.Tuple[str, File]]: | ||
""" | ||
The format we use is a list of tuples, where the first element is the | ||
name of the file and the second is the file object. Typically HTTPX wants | ||
a dict, but to be able to send lists of files, you have to use the list | ||
approach (which also works for non-lists) | ||
https://github.com/encode/httpx/pull/1032 | ||
""" | ||
|
||
httpx_tuples = [] | ||
for key, file_like in d.items(): | ||
if isinstance(file_like, list): | ||
for file_like_item in file_like: | ||
httpx_tuples.append((key, file_like_item)) | ||
else: | ||
httpx_tuples.append((key, file_like)) | ||
return httpx_tuples |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
# This file was auto-generated by Fern from our API Definition. | ||
|
||
import asyncio | ||
import email.utils | ||
import re | ||
import time | ||
import typing | ||
from functools import wraps | ||
from random import random | ||
|
||
import httpx | ||
|
||
INITIAL_RETRY_DELAY_SECONDS = 0.5 | ||
MAX_RETRY_DELAY_SECONDS = 10 | ||
MAX_RETRY_DELAY_SECONDS_FROM_HEADER = 30 | ||
|
||
|
||
def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]: | ||
""" | ||
This function parses the `Retry-After` header in a HTTP response and returns the number of seconds to wait. | ||
Inspired by the urllib3 retry implementation. | ||
""" | ||
retry_after_ms = response_headers.get("retry-after-ms") | ||
if retry_after_ms is not None: | ||
try: | ||
return int(retry_after_ms) / 1000 if retry_after_ms > 0 else 0 | ||
except Exception: | ||
pass | ||
|
||
retry_after = response_headers.get("retry-after") | ||
if retry_after is None: | ||
return None | ||
|
||
# Attempt to parse the header as an int. | ||
if re.match(r"^\s*[0-9]+\s*$", retry_after): | ||
seconds = float(retry_after) | ||
# Fallback to parsing it as a date. | ||
else: | ||
retry_date_tuple = email.utils.parsedate_tz(retry_after) | ||
if retry_date_tuple is None: | ||
return None | ||
if retry_date_tuple[9] is None: # Python 2 | ||
# Assume UTC if no timezone was specified | ||
# On Python2.7, parsedate_tz returns None for a timezone offset | ||
# instead of 0 if no timezone is given, where mktime_tz treats | ||
# a None timezone offset as local time. | ||
retry_date_tuple = retry_date_tuple[:9] + (0,) + retry_date_tuple[10:] | ||
|
||
retry_date = email.utils.mktime_tz(retry_date_tuple) | ||
seconds = retry_date - time.time() | ||
|
||
if seconds < 0: | ||
seconds = 0 | ||
|
||
return seconds | ||
|
||
|
||
def _retry_timeout(response: httpx.Response, retries: int) -> float: | ||
""" | ||
Determine the amount of time to wait before retrying a request. | ||
This function begins by trying to parse a retry-after header from the response, and then proceeds to use exponential backoff | ||
with a jitter to determine the number of seconds to wait. | ||
""" | ||
|
||
# If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says. | ||
retry_after = _parse_retry_after(response.headers) | ||
if retry_after is not None and retry_after <= MAX_RETRY_DELAY_SECONDS_FROM_HEADER: | ||
return retry_after | ||
|
||
# Apply exponential backoff, capped at MAX_RETRY_DELAY_SECONDS. | ||
retry_delay = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS) | ||
|
||
# Add a randomness / jitter to the retry delay to avoid overwhelming the server with retries. | ||
timeout = retry_delay * (1 - 0.25 * random()) | ||
return timeout if timeout >= 0 else 0 | ||
|
||
|
||
def _should_retry(response: httpx.Response) -> bool: | ||
retriable_400s = [429, 408, 409] | ||
return response.status_code >= 500 or response.status_code in retriable_400s | ||
|
||
|
||
class HttpClient: | ||
def __init__(self, *, httpx_client: httpx.Client): | ||
self.httpx_client = httpx_client | ||
|
||
# Ensure that the signature of the `request` method is the same as the `httpx.Client.request` method | ||
@wraps(httpx.Client.request) | ||
def request( | ||
self, *args: typing.Any, max_retries: int = 0, retries: int = 0, **kwargs: typing.Any | ||
) -> httpx.Response: | ||
response = self.httpx_client.request(*args, **kwargs) | ||
if _should_retry(response=response): | ||
if max_retries > retries: | ||
time.sleep(_retry_timeout(response=response, retries=retries)) | ||
return self.request(max_retries=max_retries, retries=retries + 1, *args, **kwargs) | ||
return response | ||
|
||
@wraps(httpx.Client.stream) | ||
def stream(self, *args: typing.Any, max_retries: int = 0, retries: int = 0, **kwargs: typing.Any) -> typing.Any: | ||
return self.httpx_client.stream(*args, **kwargs) | ||
|
||
|
||
class AsyncHttpClient: | ||
def __init__(self, *, httpx_client: httpx.AsyncClient): | ||
self.httpx_client = httpx_client | ||
|
||
# Ensure that the signature of the `request` method is the same as the `httpx.Client.request` method | ||
@wraps(httpx.AsyncClient.request) | ||
async def request( | ||
self, *args: typing.Any, max_retries: int = 0, retries: int = 0, **kwargs: typing.Any | ||
) -> httpx.Response: | ||
response = await self.httpx_client.request(*args, **kwargs) | ||
if _should_retry(response=response): | ||
if max_retries > retries: | ||
await asyncio.sleep(_retry_timeout(response=response, retries=retries)) | ||
return await self.request(max_retries=max_retries, retries=retries + 1, *args, **kwargs) | ||
return response | ||
|
||
@wraps(httpx.AsyncClient.request) | ||
async def stream( | ||
self, *args: typing.Any, max_retries: int = 0, retries: int = 0, **kwargs: typing.Any | ||
) -> typing.Any: | ||
return self.httpx_client.stream(*args, **kwargs) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# This file was auto-generated by Fern from our API Definition. | ||
|
||
import typing | ||
|
||
try: | ||
from typing import NotRequired # type: ignore | ||
except ImportError: | ||
from typing_extensions import NotRequired # type: ignore | ||
|
||
|
||
class RequestOptions(typing.TypedDict): | ||
""" | ||
Additional options for request-specific configuration when calling APIs via the SDK. | ||
This is used primarily as an optional final parameter for service functions. | ||
Attributes: | ||
- timeout_in_seconds: int. The number of seconds to await an API call before timing out. | ||
- max_retries: int. The max number of retries to attempt if the API call fails. | ||
- additional_headers: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's header dict | ||
- additional_query_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's query parameters dict | ||
- additional_body_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's body parameters dict | ||
""" | ||
|
||
timeout_in_seconds: NotRequired[int] | ||
max_retries: NotRequired[int] | ||
additional_headers: NotRequired[typing.Dict[str, typing.Any]] | ||
additional_query_parameters: NotRequired[typing.Dict[str, typing.Any]] | ||
additional_body_parameters: NotRequired[typing.Dict[str, typing.Any]] |
Oops, something went wrong.