diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c373724..46b9b6b 100755 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.8" + ".": "0.1.0-alpha.9" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index bf3f76e..4e11e72 100755 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 27 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/clear-street%2Fstudio-sdk-cac25d6221de76f03b4e417f9d33f9665f8ed5c2d324693a46d9506212a1fb3e.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/clear-street%2Fstudio-sdk-66efc857b146cfd0808fb37e5913e8ce46ee44fd0eac54154cfbc340d197585d.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 1066833..b119baa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 0.1.0-alpha.9 (2024-09-05) + +Full Changelog: [v0.1.0-alpha.8...v0.1.0-alpha.9](https://github.com/clear-street/studio-sdk-python/compare/v0.1.0-alpha.8...v0.1.0-alpha.9) + +### Features + +* **api:** add sandbox url ([#36](https://github.com/clear-street/studio-sdk-python/issues/36)) ([8a98682](https://github.com/clear-street/studio-sdk-python/commit/8a98682938297f756a7560f3fa76daf083b4cbe8)) + + +### Chores + +* pyproject.toml formatting changes ([#37](https://github.com/clear-street/studio-sdk-python/issues/37)) ([927355c](https://github.com/clear-street/studio-sdk-python/commit/927355cf8e5dc6f62a515e1a91b1cf5604d17002)) + ## 0.1.0-alpha.8 (2024-08-30) Full Changelog: [v0.1.0-alpha.7...v0.1.0-alpha.8](https://github.com/clear-street/studio-sdk-python/compare/v0.1.0-alpha.7...v0.1.0-alpha.8) diff --git a/README.md b/README.md index bc30637..c2dba7b 100755 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ from studio_sdk import StudioSDK client = StudioSDK( # This is the default and can be omitted bearer_token=os.environ.get("STUDIO_SDK_BEARER_TOKEN"), + # defaults to "production". + environment="sandbox", ) entity = client.entities.retrieve( @@ -55,6 +57,8 @@ from studio_sdk import AsyncStudioSDK client = AsyncStudioSDK( # This is the default and can be omitted bearer_token=os.environ.get("STUDIO_SDK_BEARER_TOKEN"), + # defaults to "production". + environment="sandbox", ) diff --git a/pyproject.toml b/pyproject.toml index 2231681..43fd734 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "clear-street-studio-sdk" -version = "0.1.0-alpha.8" +version = "0.1.0-alpha.9" description = "The official Python library for the studio-sdk API" dynamic = ["readme"] license = "Apache-2.0" @@ -15,7 +15,6 @@ dependencies = [ "distro>=1.7.0, <2", "sniffio", "cached-property; python_version < '3.8'", - ] requires-python = ">= 3.7" classifiers = [ @@ -36,8 +35,6 @@ classifiers = [ "License :: OSI Approved :: Apache Software License" ] - - [project.urls] Homepage = "https://github.com/clear-street/studio-sdk-python" Repository = "https://github.com/clear-street/studio-sdk-python" @@ -59,7 +56,6 @@ dev-dependencies = [ "dirty-equals>=0.6.0", "importlib-metadata>=6.7.0", "rich>=13.7.1", - ] [tool.rye.scripts] diff --git a/src/studio_sdk/__init__.py b/src/studio_sdk/__init__.py index 12dbebb..25644e3 100755 --- a/src/studio_sdk/__init__.py +++ b/src/studio_sdk/__init__.py @@ -4,6 +4,7 @@ from ._types import NOT_GIVEN, NoneType, NotGiven, Transport, ProxiesTypes from ._utils import file_from_path from ._client import ( + ENVIRONMENTS, Client, Stream, Timeout, @@ -68,6 +69,7 @@ "AsyncStream", "StudioSDK", "AsyncStudioSDK", + "ENVIRONMENTS", "file_from_path", "BaseModel", "DEFAULT_TIMEOUT", diff --git a/src/studio_sdk/_client.py b/src/studio_sdk/_client.py index a382f1a..253dcbc 100755 --- a/src/studio_sdk/_client.py +++ b/src/studio_sdk/_client.py @@ -3,8 +3,8 @@ from __future__ import annotations import os -from typing import Any, Union, Mapping -from typing_extensions import Self, override +from typing import Any, Dict, Union, Mapping, cast +from typing_extensions import Self, Literal, override import httpx @@ -33,6 +33,7 @@ ) __all__ = [ + "ENVIRONMENTS", "Timeout", "Transport", "ProxiesTypes", @@ -44,6 +45,11 @@ "AsyncClient", ] +ENVIRONMENTS: Dict[str, str] = { + "production": "https://api.clearstreet.io/studio/v2", + "sandbox": "https://sandbox-api.clearstreet.io/studio/v2", +} + class StudioSDK(SyncAPIClient): entities: resources.EntitiesResource @@ -55,11 +61,14 @@ class StudioSDK(SyncAPIClient): # client options bearer_token: str + _environment: Literal["production", "sandbox"] | NotGiven + def __init__( self, *, bearer_token: str | None = None, - base_url: str | httpx.URL | None = None, + environment: Literal["production", "sandbox"] | NotGiven = NOT_GIVEN, + base_url: str | httpx.URL | None | NotGiven = NOT_GIVEN, timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, @@ -90,10 +99,31 @@ def __init__( ) self.bearer_token = bearer_token - if base_url is None: - base_url = os.environ.get("STUDIO_SDK_BASE_URL") - if base_url is None: - base_url = f"https://api.clearstreet.io/studio/v2" + self._environment = environment + + base_url_env = os.environ.get("STUDIO_SDK_BASE_URL") + if is_given(base_url) and base_url is not None: + # cast required because mypy doesn't understand the type narrowing + base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] + elif is_given(environment): + if base_url_env and base_url is not None: + raise ValueError( + "Ambiguous URL; The `STUDIO_SDK_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", + ) + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc + elif base_url_env is not None: + base_url = base_url_env + else: + self._environment = environment = "production" + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc super().__init__( version=__version__, @@ -136,6 +166,7 @@ def copy( self, *, bearer_token: str | None = None, + environment: Literal["production", "sandbox"] | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, http_client: httpx.Client | None = None, @@ -171,6 +202,7 @@ def copy( return self.__class__( bearer_token=bearer_token or self.bearer_token, base_url=base_url or self.base_url, + environment=environment or self._environment, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, http_client=http_client, max_retries=max_retries if is_given(max_retries) else self.max_retries, @@ -227,11 +259,14 @@ class AsyncStudioSDK(AsyncAPIClient): # client options bearer_token: str + _environment: Literal["production", "sandbox"] | NotGiven + def __init__( self, *, bearer_token: str | None = None, - base_url: str | httpx.URL | None = None, + environment: Literal["production", "sandbox"] | NotGiven = NOT_GIVEN, + base_url: str | httpx.URL | None | NotGiven = NOT_GIVEN, timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, @@ -262,10 +297,31 @@ def __init__( ) self.bearer_token = bearer_token - if base_url is None: - base_url = os.environ.get("STUDIO_SDK_BASE_URL") - if base_url is None: - base_url = f"https://api.clearstreet.io/studio/v2" + self._environment = environment + + base_url_env = os.environ.get("STUDIO_SDK_BASE_URL") + if is_given(base_url) and base_url is not None: + # cast required because mypy doesn't understand the type narrowing + base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] + elif is_given(environment): + if base_url_env and base_url is not None: + raise ValueError( + "Ambiguous URL; The `STUDIO_SDK_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", + ) + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc + elif base_url_env is not None: + base_url = base_url_env + else: + self._environment = environment = "production" + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc super().__init__( version=__version__, @@ -308,6 +364,7 @@ def copy( self, *, bearer_token: str | None = None, + environment: Literal["production", "sandbox"] | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, http_client: httpx.AsyncClient | None = None, @@ -343,6 +400,7 @@ def copy( return self.__class__( bearer_token=bearer_token or self.bearer_token, base_url=base_url or self.base_url, + environment=environment or self._environment, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, http_client=http_client, max_retries=max_retries if is_given(max_retries) else self.max_retries, diff --git a/src/studio_sdk/_version.py b/src/studio_sdk/_version.py index 26c3297..a6ba505 100755 --- 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.8" # x-release-please-version +__version__ = "0.1.0-alpha.9" # x-release-please-version diff --git a/tests/test_client.py b/tests/test_client.py index 2ee6464..d86ab14 100755 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -564,6 +564,16 @@ def test_base_url_env(self) -> None: client = StudioSDK(bearer_token=bearer_token, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" + # explicit environment arg requires explicitness + with update_env(STUDIO_SDK_BASE_URL="http://localhost:5000/from/env"): + with pytest.raises(ValueError, match=r"you must pass base_url=None"): + StudioSDK(bearer_token=bearer_token, _strict_response_validation=True, environment="production") + + client = StudioSDK( + base_url=None, bearer_token=bearer_token, _strict_response_validation=True, environment="production" + ) + assert str(client.base_url).startswith("https://api.clearstreet.io/studio/v2") + @pytest.mark.parametrize( "client", [ @@ -1287,6 +1297,16 @@ def test_base_url_env(self) -> None: client = AsyncStudioSDK(bearer_token=bearer_token, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" + # explicit environment arg requires explicitness + with update_env(STUDIO_SDK_BASE_URL="http://localhost:5000/from/env"): + with pytest.raises(ValueError, match=r"you must pass base_url=None"): + AsyncStudioSDK(bearer_token=bearer_token, _strict_response_validation=True, environment="production") + + client = AsyncStudioSDK( + base_url=None, bearer_token=bearer_token, _strict_response_validation=True, environment="production" + ) + assert str(client.base_url).startswith("https://api.clearstreet.io/studio/v2") + @pytest.mark.parametrize( "client", [