diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 08e82c4..7e56fe2 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.15" + ".": "0.1.0-alpha.16" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d70889b..4c7d64d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 0.1.0-alpha.16 (2024-12-18) + +Full Changelog: [v0.1.0-alpha.15...v0.1.0-alpha.16](https://github.com/clear-street/studio-sdk-python/compare/v0.1.0-alpha.15...v0.1.0-alpha.16) + +### Chores + +* **internal:** add support for TypeAliasType ([#80](https://github.com/clear-street/studio-sdk-python/issues/80)) ([174dfac](https://github.com/clear-street/studio-sdk-python/commit/174dfac4c311728e5e80dd60828671248078a8d8)) +* **internal:** bump pydantic dependency ([#76](https://github.com/clear-street/studio-sdk-python/issues/76)) ([9f6bfa1](https://github.com/clear-street/studio-sdk-python/commit/9f6bfa14408def30dcc33d4953faf017111b75a8)) +* **internal:** bump pyright ([#79](https://github.com/clear-street/studio-sdk-python/issues/79)) ([78f1099](https://github.com/clear-street/studio-sdk-python/commit/78f10998fab7d9789b01e8c68e9c376cef802189)) +* **internal:** codegen related update ([#81](https://github.com/clear-street/studio-sdk-python/issues/81)) ([5ebf768](https://github.com/clear-street/studio-sdk-python/commit/5ebf7684b4a459b3c070df3758bab4ce4ae4fac7)) +* **internal:** fix some typos ([#83](https://github.com/clear-street/studio-sdk-python/issues/83)) ([2f9c6fa](https://github.com/clear-street/studio-sdk-python/commit/2f9c6faa24900512a7b40c25256fe8dee0d5003b)) + + +### Documentation + +* **readme:** example snippet for client context manager ([#82](https://github.com/clear-street/studio-sdk-python/issues/82)) ([6d2b4a7](https://github.com/clear-street/studio-sdk-python/commit/6d2b4a7060ca8d6652ea559760657c58e7be0618)) +* **readme:** fix http client proxies example ([#78](https://github.com/clear-street/studio-sdk-python/issues/78)) ([d3e3918](https://github.com/clear-street/studio-sdk-python/commit/d3e3918a7e9e29776a017fc06ee2ed6c762eaec2)) + ## 0.1.0-alpha.15 (2024-12-09) Full Changelog: [v0.1.0-alpha.14...v0.1.0-alpha.15](https://github.com/clear-street/studio-sdk-python/compare/v0.1.0-alpha.14...v0.1.0-alpha.15) diff --git a/README.md b/README.md index a86ec70..70ba84e 100644 --- a/README.md +++ b/README.md @@ -284,18 +284,19 @@ can also get all the extra fields on the Pydantic model as a dict with You can directly override the [httpx client](https://www.python-httpx.org/api/#client) to customize it for your use case, including: -- Support for proxies -- Custom transports +- Support for [proxies](https://www.python-httpx.org/advanced/proxies/) +- Custom [transports](https://www.python-httpx.org/advanced/transports/) - Additional [advanced](https://www.python-httpx.org/advanced/clients/) functionality ```python +import httpx from studio_sdk import StudioSDK, DefaultHttpxClient client = StudioSDK( # Or use the `STUDIO_SDK_BASE_URL` env var base_url="http://my.test.server.example.com:8083", http_client=DefaultHttpxClient( - proxies="http://my.test.proxy.example.com", + proxy="http://my.test.proxy.example.com", transport=httpx.HTTPTransport(local_address="0.0.0.0"), ), ) @@ -311,6 +312,16 @@ client.with_options(http_client=DefaultHttpxClient(...)) By default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting. +```py +from studio_sdk import StudioSDK + +with StudioSDK() as client: + # make requests here + ... + +# HTTP client is now closed +``` + ## Versioning This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions: diff --git a/pyproject.toml b/pyproject.toml index 845b1af..aa1de30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "clear-street-studio-sdk" -version = "0.1.0-alpha.15" +version = "0.1.0-alpha.16" description = "The official Python library for the studio-sdk API" dynamic = ["readme"] license = "Apache-2.0" @@ -10,7 +10,7 @@ authors = [ dependencies = [ "httpx>=0.23.0, <1", "pydantic>=1.9.0, <3", - "typing-extensions>=4.7, <5", + "typing-extensions>=4.10, <5", "anyio>=3.5.0, <5", "distro>=1.7.0, <2", "sniffio", diff --git a/requirements-dev.lock b/requirements-dev.lock index 0df92fe..0b3696e 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -62,13 +62,13 @@ platformdirs==3.11.0 # via virtualenv pluggy==1.5.0 # via pytest -pydantic==2.9.2 +pydantic==2.10.3 # via clear-street-studio-sdk -pydantic-core==2.23.4 +pydantic-core==2.27.1 # via pydantic pygments==2.18.0 # via rich -pyright==1.1.389 +pyright==1.1.390 pytest==8.3.3 # via pytest-asyncio pytest-asyncio==0.24.0 diff --git a/requirements.lock b/requirements.lock index 844da47..7356e3c 100644 --- a/requirements.lock +++ b/requirements.lock @@ -30,9 +30,9 @@ httpx==0.25.2 idna==3.4 # via anyio # via httpx -pydantic==2.9.2 +pydantic==2.10.3 # via clear-street-studio-sdk -pydantic-core==2.23.4 +pydantic-core==2.27.1 # via pydantic sniffio==1.3.0 # via anyio diff --git a/src/studio_sdk/_client.py b/src/studio_sdk/_client.py index 253dcbc..a10763a 100644 --- a/src/studio_sdk/_client.py +++ b/src/studio_sdk/_client.py @@ -8,7 +8,7 @@ import httpx -from . import resources, _exceptions +from . import _exceptions from ._qs import Querystring from ._types import ( NOT_GIVEN, @@ -24,6 +24,7 @@ get_async_library, ) from ._version import __version__ +from .resources import instruments from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import APIStatusError, StudioSDKError from ._base_client import ( @@ -31,6 +32,8 @@ SyncAPIClient, AsyncAPIClient, ) +from .resources.accounts import accounts +from .resources.entities import entities __all__ = [ "ENVIRONMENTS", @@ -38,7 +41,6 @@ "Transport", "ProxiesTypes", "RequestOptions", - "resources", "StudioSDK", "AsyncStudioSDK", "Client", @@ -52,9 +54,9 @@ class StudioSDK(SyncAPIClient): - entities: resources.EntitiesResource - accounts: resources.AccountsResource - instruments: resources.InstrumentsResource + entities: entities.EntitiesResource + accounts: accounts.AccountsResource + instruments: instruments.InstrumentsResource with_raw_response: StudioSDKWithRawResponse with_streaming_response: StudioSDKWithStreamedResponse @@ -136,9 +138,9 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.entities = resources.EntitiesResource(self) - self.accounts = resources.AccountsResource(self) - self.instruments = resources.InstrumentsResource(self) + self.entities = entities.EntitiesResource(self) + self.accounts = accounts.AccountsResource(self) + self.instruments = instruments.InstrumentsResource(self) self.with_raw_response = StudioSDKWithRawResponse(self) self.with_streaming_response = StudioSDKWithStreamedResponse(self) @@ -250,9 +252,9 @@ def _make_status_error( class AsyncStudioSDK(AsyncAPIClient): - entities: resources.AsyncEntitiesResource - accounts: resources.AsyncAccountsResource - instruments: resources.AsyncInstrumentsResource + entities: entities.AsyncEntitiesResource + accounts: accounts.AsyncAccountsResource + instruments: instruments.AsyncInstrumentsResource with_raw_response: AsyncStudioSDKWithRawResponse with_streaming_response: AsyncStudioSDKWithStreamedResponse @@ -334,9 +336,9 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.entities = resources.AsyncEntitiesResource(self) - self.accounts = resources.AsyncAccountsResource(self) - self.instruments = resources.AsyncInstrumentsResource(self) + self.entities = entities.AsyncEntitiesResource(self) + self.accounts = accounts.AsyncAccountsResource(self) + self.instruments = instruments.AsyncInstrumentsResource(self) self.with_raw_response = AsyncStudioSDKWithRawResponse(self) self.with_streaming_response = AsyncStudioSDKWithStreamedResponse(self) @@ -449,30 +451,30 @@ def _make_status_error( class StudioSDKWithRawResponse: def __init__(self, client: StudioSDK) -> None: - self.entities = resources.EntitiesResourceWithRawResponse(client.entities) - self.accounts = resources.AccountsResourceWithRawResponse(client.accounts) - self.instruments = resources.InstrumentsResourceWithRawResponse(client.instruments) + self.entities = entities.EntitiesResourceWithRawResponse(client.entities) + self.accounts = accounts.AccountsResourceWithRawResponse(client.accounts) + self.instruments = instruments.InstrumentsResourceWithRawResponse(client.instruments) class AsyncStudioSDKWithRawResponse: def __init__(self, client: AsyncStudioSDK) -> None: - self.entities = resources.AsyncEntitiesResourceWithRawResponse(client.entities) - self.accounts = resources.AsyncAccountsResourceWithRawResponse(client.accounts) - self.instruments = resources.AsyncInstrumentsResourceWithRawResponse(client.instruments) + self.entities = entities.AsyncEntitiesResourceWithRawResponse(client.entities) + self.accounts = accounts.AsyncAccountsResourceWithRawResponse(client.accounts) + self.instruments = instruments.AsyncInstrumentsResourceWithRawResponse(client.instruments) class StudioSDKWithStreamedResponse: def __init__(self, client: StudioSDK) -> None: - self.entities = resources.EntitiesResourceWithStreamingResponse(client.entities) - self.accounts = resources.AccountsResourceWithStreamingResponse(client.accounts) - self.instruments = resources.InstrumentsResourceWithStreamingResponse(client.instruments) + self.entities = entities.EntitiesResourceWithStreamingResponse(client.entities) + self.accounts = accounts.AccountsResourceWithStreamingResponse(client.accounts) + self.instruments = instruments.InstrumentsResourceWithStreamingResponse(client.instruments) class AsyncStudioSDKWithStreamedResponse: def __init__(self, client: AsyncStudioSDK) -> None: - self.entities = resources.AsyncEntitiesResourceWithStreamingResponse(client.entities) - self.accounts = resources.AsyncAccountsResourceWithStreamingResponse(client.accounts) - self.instruments = resources.AsyncInstrumentsResourceWithStreamingResponse(client.instruments) + self.entities = entities.AsyncEntitiesResourceWithStreamingResponse(client.entities) + self.accounts = accounts.AsyncAccountsResourceWithStreamingResponse(client.accounts) + self.instruments = instruments.AsyncInstrumentsResourceWithStreamingResponse(client.instruments) Client = StudioSDK diff --git a/src/studio_sdk/_models.py b/src/studio_sdk/_models.py index 6cb469e..7a547ce 100644 --- a/src/studio_sdk/_models.py +++ b/src/studio_sdk/_models.py @@ -46,6 +46,7 @@ strip_not_given, extract_type_arg, is_annotated_type, + is_type_alias_type, strip_annotated_type, ) from ._compat import ( @@ -428,6 +429,8 @@ def construct_type(*, value: object, type_: object) -> object: # we allow `object` as the input type because otherwise, passing things like # `Literal['value']` will be reported as a type error by type checkers type_ = cast("type[object]", type_) + if is_type_alias_type(type_): + type_ = type_.__value__ # type: ignore[unreachable] # unwrap `Annotated[T, ...]` -> `T` if is_annotated_type(type_): diff --git a/src/studio_sdk/_response.py b/src/studio_sdk/_response.py index 7d999c4..6405ec2 100644 --- a/src/studio_sdk/_response.py +++ b/src/studio_sdk/_response.py @@ -25,7 +25,7 @@ import pydantic from ._types import NoneType -from ._utils import is_given, extract_type_arg, is_annotated_type, extract_type_var_from_base +from ._utils import is_given, extract_type_arg, is_annotated_type, is_type_alias_type, extract_type_var_from_base from ._models import BaseModel, is_basemodel from ._constants import RAW_RESPONSE_HEADER, OVERRIDE_CAST_TO_HEADER from ._streaming import Stream, AsyncStream, is_stream_class_type, extract_stream_chunk_type @@ -126,9 +126,15 @@ def __repr__(self) -> str: ) def _parse(self, *, to: type[_T] | None = None) -> R | _T: + cast_to = to if to is not None else self._cast_to + + # unwrap `TypeAlias('Name', T)` -> `T` + if is_type_alias_type(cast_to): + cast_to = cast_to.__value__ # type: ignore[unreachable] + # unwrap `Annotated[T, ...]` -> `T` - if to and is_annotated_type(to): - to = extract_type_arg(to, 0) + if cast_to and is_annotated_type(cast_to): + cast_to = extract_type_arg(cast_to, 0) if self._is_sse_stream: if to: @@ -164,18 +170,12 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: return cast( R, stream_cls( - cast_to=self._cast_to, + cast_to=cast_to, response=self.http_response, client=cast(Any, self._client), ), ) - cast_to = to if to is not None else self._cast_to - - # unwrap `Annotated[T, ...]` -> `T` - if is_annotated_type(cast_to): - cast_to = extract_type_arg(cast_to, 0) - if cast_to is NoneType: return cast(R, None) diff --git a/src/studio_sdk/_types.py b/src/studio_sdk/_types.py index 695ce5a..00f4a51 100644 --- a/src/studio_sdk/_types.py +++ b/src/studio_sdk/_types.py @@ -192,10 +192,8 @@ def get(self, __key: str) -> str | None: ... StrBytesIntFloat = Union[str, bytes, int, float] # Note: copied from Pydantic -# https://github.com/pydantic/pydantic/blob/32ea570bf96e84234d2992e1ddf40ab8a565925a/pydantic/main.py#L49 -IncEx: TypeAlias = Union[ - Set[int], Set[str], Mapping[int, Union["IncEx", Literal[True]]], Mapping[str, Union["IncEx", Literal[True]]] -] +# https://github.com/pydantic/pydantic/blob/6f31f8f68ef011f84357330186f603ff295312fd/pydantic/main.py#L79 +IncEx: TypeAlias = Union[Set[int], Set[str], Mapping[int, Union["IncEx", bool]], Mapping[str, Union["IncEx", bool]]] PostParser = Callable[[Any], Any] diff --git a/src/studio_sdk/_utils/__init__.py b/src/studio_sdk/_utils/__init__.py index a7cff3c..d4fda26 100644 --- a/src/studio_sdk/_utils/__init__.py +++ b/src/studio_sdk/_utils/__init__.py @@ -39,6 +39,7 @@ is_iterable_type as is_iterable_type, is_required_type as is_required_type, is_annotated_type as is_annotated_type, + is_type_alias_type as is_type_alias_type, strip_annotated_type as strip_annotated_type, extract_type_var_from_base as extract_type_var_from_base, ) diff --git a/src/studio_sdk/_utils/_typing.py b/src/studio_sdk/_utils/_typing.py index c036991..278749b 100644 --- a/src/studio_sdk/_utils/_typing.py +++ b/src/studio_sdk/_utils/_typing.py @@ -1,8 +1,17 @@ from __future__ import annotations +import sys +import typing +import typing_extensions from typing import Any, TypeVar, Iterable, cast from collections import abc as _c_abc -from typing_extensions import Required, Annotated, get_args, get_origin +from typing_extensions import ( + TypeIs, + Required, + Annotated, + get_args, + get_origin, +) from .._types import InheritsGeneric from .._compat import is_union as _is_union @@ -36,6 +45,26 @@ def is_typevar(typ: type) -> bool: return type(typ) == TypeVar # type: ignore +_TYPE_ALIAS_TYPES: tuple[type[typing_extensions.TypeAliasType], ...] = (typing_extensions.TypeAliasType,) +if sys.version_info >= (3, 12): + _TYPE_ALIAS_TYPES = (*_TYPE_ALIAS_TYPES, typing.TypeAliasType) + + +def is_type_alias_type(tp: Any, /) -> TypeIs[typing_extensions.TypeAliasType]: + """Return whether the provided argument is an instance of `TypeAliasType`. + + ```python + type Int = int + is_type_alias_type(Int) + # > True + Str = TypeAliasType("Str", str) + is_type_alias_type(Str) + # > True + ``` + """ + return isinstance(tp, _TYPE_ALIAS_TYPES) + + # 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): diff --git a/src/studio_sdk/_version.py b/src/studio_sdk/_version.py index c4d2c9e..a0f6be5 100644 --- a/src/studio_sdk/_version.py +++ b/src/studio_sdk/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "studio_sdk" -__version__ = "0.1.0-alpha.15" # x-release-please-version +__version__ = "0.1.0-alpha.16" # x-release-please-version diff --git a/tests/test_client.py b/tests/test_client.py index cafac71..2f8b7c4 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -364,11 +364,11 @@ def test_default_query_option(self) -> None: FinalRequestOptions( method="get", url="/foo", - params={"foo": "baz", "query_param": "overriden"}, + params={"foo": "baz", "query_param": "overridden"}, ) ) url = httpx.URL(request.url) - assert dict(url.params) == {"foo": "baz", "query_param": "overriden"} + assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} def test_request_extra_json(self) -> None: request = self.client._build_request( @@ -1154,11 +1154,11 @@ def test_default_query_option(self) -> None: FinalRequestOptions( method="get", url="/foo", - params={"foo": "baz", "query_param": "overriden"}, + params={"foo": "baz", "query_param": "overridden"}, ) ) url = httpx.URL(request.url) - assert dict(url.params) == {"foo": "baz", "query_param": "overriden"} + assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} def test_request_extra_json(self) -> None: request = self.client._build_request( diff --git a/tests/test_models.py b/tests/test_models.py index dd2d76a..c3de7db 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,7 +1,7 @@ import json from typing import Any, Dict, List, Union, Optional, cast from datetime import datetime, timezone -from typing_extensions import Literal, Annotated +from typing_extensions import Literal, Annotated, TypeAliasType import pytest import pydantic @@ -828,3 +828,19 @@ class B(BaseModel): # if the discriminator details object stays the same between invocations then # we hit the cache assert UnionType.__discriminator__ is discriminator + + +@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1") +def test_type_alias_type() -> None: + Alias = TypeAliasType("Alias", str) + + class Model(BaseModel): + alias: Alias + union: Union[int, Alias] + + m = construct_type(value={"alias": "foo", "union": "bar"}, type_=Model) + assert isinstance(m, Model) + assert isinstance(m.alias, str) + assert m.alias == "foo" + assert isinstance(m.union, str) + assert m.union == "bar" diff --git a/tests/utils.py b/tests/utils.py index 48c82a9..70de219 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -16,6 +16,7 @@ is_union_type, extract_type_arg, is_annotated_type, + is_type_alias_type, ) from studio_sdk._compat import PYDANTIC_V2, field_outer_type, get_model_fields from studio_sdk._models import BaseModel @@ -51,6 +52,9 @@ def assert_matches_type( path: list[str], allow_none: bool = False, ) -> None: + if is_type_alias_type(type_): + type_ = type_.__value__ + # unwrap `Annotated[T, ...]` -> `T` if is_annotated_type(type_): type_ = extract_type_arg(type_, 0)