Skip to content

release: 1.39.1 #141

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/handle-release-pr-title-edit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ jobs:
startsWith(github.event.pull_request.head.ref, 'release-please--') &&
github.event.pull_request.state == 'open' &&
github.event.sender.login != 'stainless-bot' &&
github.event.sender.login != 'stainless-app' &&
github.repository == 'orbcorp/orb-python'
runs-on: ubuntu-latest
steps:
Expand Down
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "1.39.0"
".": "1.39.1"
}
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Changelog

## 1.39.1 (2024-01-18)

Full Changelog: [v1.39.0...v1.39.1](https://github.com/orbcorp/orb-python/compare/v1.39.0...v1.39.1)

### Bug Fixes

* **ci:** ignore stainless-app edits to release PR title ([#146](https://github.com/orbcorp/orb-python/issues/146)) ([5a2e2b5](https://github.com/orbcorp/orb-python/commit/5a2e2b58b7763a99907ae3a3a8afd6a19789cb62))


### Chores

* **internal:** fix typing util function ([#140](https://github.com/orbcorp/orb-python/issues/140)) ([31a83a9](https://github.com/orbcorp/orb-python/commit/31a83a9d24ba25004ab234dc1b555a03d7333a1f))
* **internal:** remove redundant client test ([#142](https://github.com/orbcorp/orb-python/issues/142)) ([e315f4e](https://github.com/orbcorp/orb-python/commit/e315f4e00c4e2a45a657d56df3a22813264632a3))
* **internal:** share client instances between all tests ([#145](https://github.com/orbcorp/orb-python/issues/145)) ([64a4cb6](https://github.com/orbcorp/orb-python/commit/64a4cb6dfc3b5947701dba25dbe8c37b6050eb38))
* **internal:** speculative retry-after-ms support ([#143](https://github.com/orbcorp/orb-python/issues/143)) ([d7affe9](https://github.com/orbcorp/orb-python/commit/d7affe9e3fae951027d43307df10420143e69042))
* lazy load raw resource class properties ([#144](https://github.com/orbcorp/orb-python/issues/144)) ([72734a3](https://github.com/orbcorp/orb-python/commit/72734a340d158aae1e039d615a72bff2b9d89454))

## 1.39.0 (2024-01-17)

Full Changelog: [v1.38.1...v1.39.0](https://github.com/orbcorp/orb-python/compare/v1.38.1...v1.39.0)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "orb-billing"
version = "1.39.0"
version = "1.39.1"
description = "The official Python library for the orb API"
readme = "README.md"
license = "Apache-2.0"
Expand Down
66 changes: 39 additions & 27 deletions src/orb/_base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@
from ._constants import (
DEFAULT_LIMITS,
DEFAULT_TIMEOUT,
MAX_RETRY_DELAY,
DEFAULT_MAX_RETRIES,
INITIAL_RETRY_DELAY,
RAW_RESPONSE_HEADER,
OVERRIDE_CAST_TO_HEADER,
)
Expand Down Expand Up @@ -590,47 +592,57 @@ def base_url(self, url: URL | str) -> None:
def platform_headers(self) -> Dict[str, str]:
return platform_headers(self._version)

def _parse_retry_after_header(self, response_headers: Optional[httpx.Headers] = None) -> float | None:
"""Returns a float of the number of seconds (not milliseconds) to wait after retrying, or None if unspecified.

About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
See also https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After#syntax
"""
if response_headers is None:
return None

# First, try the non-standard `retry-after-ms` header for milliseconds,
# which is more precise than integer-seconds `retry-after`
try:
retry_ms_header = response_headers.get("retry-after-ms", None)
return float(retry_ms_header) / 1000
except (TypeError, ValueError):
pass

# Next, try parsing `retry-after` header as seconds (allowing nonstandard floats).
retry_header = response_headers.get("retry-after")
try:
# note: the spec indicates that this should only ever be an integer
# but if someone sends a float there's no reason for us to not respect it
return float(retry_header)
except (TypeError, ValueError):
pass

# Last, try parsing `retry-after` as a date.
retry_date_tuple = email.utils.parsedate_tz(retry_header)
if retry_date_tuple is None:
return None

retry_date = email.utils.mktime_tz(retry_date_tuple)
return float(retry_date - time.time())

def _calculate_retry_timeout(
self,
remaining_retries: int,
options: FinalRequestOptions,
response_headers: Optional[httpx.Headers] = None,
) -> float:
max_retries = options.get_max_retries(self.max_retries)
try:
# About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
#
# <http-date>". See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After#syntax for
# details.
if response_headers is not None:
retry_header = response_headers.get("retry-after")
try:
# note: the spec indicates that this should only ever be an integer
# but if someone sends a float there's no reason for us to not respect it
retry_after = float(retry_header)
except Exception:
retry_date_tuple = email.utils.parsedate_tz(retry_header)
if retry_date_tuple is None:
retry_after = -1
else:
retry_date = email.utils.mktime_tz(retry_date_tuple)
retry_after = int(retry_date - time.time())
else:
retry_after = -1

except Exception:
retry_after = -1

# If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says.
if 0 < retry_after <= 60:
retry_after = self._parse_retry_after_header(response_headers)
if retry_after is not None and 0 < retry_after <= 60:
return retry_after

initial_retry_delay = 0.5
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(2.0, nb_retries), 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 = 1 - 0.25 * random()
Expand Down
3 changes: 3 additions & 0 deletions src/orb/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@
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)

INITIAL_RETRY_DELAY = 0.5
MAX_RETRY_DELAY = 8.0
31 changes: 29 additions & 2 deletions src/orb/_utils/_typing.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import Any, cast
from typing import Any, TypeVar, cast
from typing_extensions import Required, Annotated, get_args, get_origin

from .._types import InheritsGeneric
Expand All @@ -23,6 +23,12 @@ def is_required_type(typ: type) -> bool:
return get_origin(typ) == Required


def is_typevar(typ: type) -> bool:
# type ignore is required because type checkers
# think this expression will always return False
return type(typ) == TypeVar # type: ignore


# Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]]
def strip_annotated_type(typ: type) -> type:
if is_required_type(typ) or is_annotated_type(typ):
Expand All @@ -49,6 +55,15 @@ class MyResponse(Foo[bytes]):

extract_type_var(MyResponse, bases=(Foo,), index=0) -> bytes
```

And where a generic subclass is given:
```py
_T = TypeVar('_T')
class MyResponse(Foo[_T]):
...

extract_type_var(MyResponse[bytes], bases=(Foo,), index=0) -> bytes
```
"""
cls = cast(object, get_origin(typ) or typ)
if cls in generic_bases:
Expand All @@ -75,6 +90,18 @@ class MyResponse(Foo[bytes]):
f"Does {cls} inherit from one of {generic_bases} ?"
)

return extract_type_arg(target_base_class, index)
extracted = extract_type_arg(target_base_class, index)
if is_typevar(extracted):
# If the extracted type argument is itself a type variable
# then that means the subclass itself is generic, so we have
# to resolve the type argument from the class itself, not
# the base class.
#
# Note: if there is more than 1 type argument, the subclass could
# change the ordering of the type arguments, this is not currently
# supported.
return extract_type_arg(typ, index)

return extracted

raise RuntimeError(f"Could not resolve inner type variable at index {index} for {typ}")
2 changes: 1 addition & 1 deletion src/orb/_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless.

__title__ = "orb"
__version__ = "1.39.0" # x-release-please-version
__version__ = "1.39.1" # x-release-please-version
24 changes: 20 additions & 4 deletions src/orb/resources/beta/beta.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,35 @@ def with_streaming_response(self) -> AsyncBetaWithStreamingResponse:

class BetaWithRawResponse:
def __init__(self, beta: Beta) -> None:
self.price = PriceWithRawResponse(beta.price)
self._beta = beta

@cached_property
def price(self) -> PriceWithRawResponse:
return PriceWithRawResponse(self._beta.price)


class AsyncBetaWithRawResponse:
def __init__(self, beta: AsyncBeta) -> None:
self.price = AsyncPriceWithRawResponse(beta.price)
self._beta = beta

@cached_property
def price(self) -> AsyncPriceWithRawResponse:
return AsyncPriceWithRawResponse(self._beta.price)


class BetaWithStreamingResponse:
def __init__(self, beta: Beta) -> None:
self.price = PriceWithStreamingResponse(beta.price)
self._beta = beta

@cached_property
def price(self) -> PriceWithStreamingResponse:
return PriceWithStreamingResponse(self._beta.price)


class AsyncBetaWithStreamingResponse:
def __init__(self, beta: AsyncBeta) -> None:
self.price = AsyncPriceWithStreamingResponse(beta.price)
self._beta = beta

@cached_property
def price(self) -> AsyncPriceWithStreamingResponse:
return AsyncPriceWithStreamingResponse(self._beta.price)
8 changes: 8 additions & 0 deletions src/orb/resources/beta/price.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,27 +229,35 @@ async def evaluate(

class PriceWithRawResponse:
def __init__(self, price: Price) -> None:
self._price = price

self.evaluate = _legacy_response.to_raw_response_wrapper(
price.evaluate,
)


class AsyncPriceWithRawResponse:
def __init__(self, price: AsyncPrice) -> None:
self._price = price

self.evaluate = _legacy_response.async_to_raw_response_wrapper(
price.evaluate,
)


class PriceWithStreamingResponse:
def __init__(self, price: Price) -> None:
self._price = price

self.evaluate = to_streamed_response_wrapper(
price.evaluate,
)


class AsyncPriceWithStreamingResponse:
def __init__(self, price: AsyncPrice) -> None:
self._price = price

self.evaluate = async_to_streamed_response_wrapper(
price.evaluate,
)
24 changes: 20 additions & 4 deletions src/orb/resources/coupons/coupons.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ async def fetch(

class CouponsWithRawResponse:
def __init__(self, coupons: Coupons) -> None:
self.subscriptions = SubscriptionsWithRawResponse(coupons.subscriptions)
self._coupons = coupons

self.create = _legacy_response.to_raw_response_wrapper(
coupons.create,
Expand All @@ -475,10 +475,14 @@ def __init__(self, coupons: Coupons) -> None:
coupons.fetch,
)

@cached_property
def subscriptions(self) -> SubscriptionsWithRawResponse:
return SubscriptionsWithRawResponse(self._coupons.subscriptions)


class AsyncCouponsWithRawResponse:
def __init__(self, coupons: AsyncCoupons) -> None:
self.subscriptions = AsyncSubscriptionsWithRawResponse(coupons.subscriptions)
self._coupons = coupons

self.create = _legacy_response.async_to_raw_response_wrapper(
coupons.create,
Expand All @@ -493,10 +497,14 @@ def __init__(self, coupons: AsyncCoupons) -> None:
coupons.fetch,
)

@cached_property
def subscriptions(self) -> AsyncSubscriptionsWithRawResponse:
return AsyncSubscriptionsWithRawResponse(self._coupons.subscriptions)


class CouponsWithStreamingResponse:
def __init__(self, coupons: Coupons) -> None:
self.subscriptions = SubscriptionsWithStreamingResponse(coupons.subscriptions)
self._coupons = coupons

self.create = to_streamed_response_wrapper(
coupons.create,
Expand All @@ -511,10 +519,14 @@ def __init__(self, coupons: Coupons) -> None:
coupons.fetch,
)

@cached_property
def subscriptions(self) -> SubscriptionsWithStreamingResponse:
return SubscriptionsWithStreamingResponse(self._coupons.subscriptions)


class AsyncCouponsWithStreamingResponse:
def __init__(self, coupons: AsyncCoupons) -> None:
self.subscriptions = AsyncSubscriptionsWithStreamingResponse(coupons.subscriptions)
self._coupons = coupons

self.create = async_to_streamed_response_wrapper(
coupons.create,
Expand All @@ -528,3 +540,7 @@ def __init__(self, coupons: AsyncCoupons) -> None:
self.fetch = async_to_streamed_response_wrapper(
coupons.fetch,
)

@cached_property
def subscriptions(self) -> AsyncSubscriptionsWithStreamingResponse:
return AsyncSubscriptionsWithStreamingResponse(self._coupons.subscriptions)
8 changes: 8 additions & 0 deletions src/orb/resources/coupons/subscriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,27 +153,35 @@ def list(

class SubscriptionsWithRawResponse:
def __init__(self, subscriptions: Subscriptions) -> None:
self._subscriptions = subscriptions

self.list = _legacy_response.to_raw_response_wrapper(
subscriptions.list,
)


class AsyncSubscriptionsWithRawResponse:
def __init__(self, subscriptions: AsyncSubscriptions) -> None:
self._subscriptions = subscriptions

self.list = _legacy_response.async_to_raw_response_wrapper(
subscriptions.list,
)


class SubscriptionsWithStreamingResponse:
def __init__(self, subscriptions: Subscriptions) -> None:
self._subscriptions = subscriptions

self.list = to_streamed_response_wrapper(
subscriptions.list,
)


class AsyncSubscriptionsWithStreamingResponse:
def __init__(self, subscriptions: AsyncSubscriptions) -> None:
self._subscriptions = subscriptions

self.list = async_to_streamed_response_wrapper(
subscriptions.list,
)
Loading