diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 55f722d..ba6c348 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.0.1-alpha.1" + ".": "0.1.0-alpha.1" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fd1943..fb1606d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.1.0-alpha.1 (2024-11-29) + +Full Changelog: [v0.0.1-alpha.1...v0.1.0-alpha.1](https://github.com/makegov/tango-python/compare/v0.0.1-alpha.1...v0.1.0-alpha.1) + +### Features + +* **api:** update via SDK Studio ([#4](https://github.com/makegov/tango-python/issues/4)) ([53ec9bd](https://github.com/makegov/tango-python/commit/53ec9bd1e1938224f9fd6ff0036fd61f3bbbcc67)) + ## 0.0.1-alpha.1 (2024-11-29) Full Changelog: [v0.0.1-alpha.0...v0.0.1-alpha.1](https://github.com/makegov/tango-python/compare/v0.0.1-alpha.0...v0.0.1-alpha.1) diff --git a/README.md b/README.md index c5e12ed..0240778 100644 --- a/README.md +++ b/README.md @@ -24,27 +24,32 @@ pip install --pre tango-python The full API of this library can be found in [api.md](api.md). ```python +import os from tango import Tango client = Tango( - client_id="My Client ID", - client_secret="My Client Secret", + access_token=os.environ.get("TANGO_API_ACCESS_TOKEN"), # This is the default and can be omitted ) schema = client.schemas.retrieve() ``` +While you can provide a `access_token` keyword argument, +we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/) +to add `TANGO_API_ACCESS_TOKEN="My Access Token"` to your `.env` file +so that your Access Token is not stored in source control. + ## Async usage Simply import `AsyncTango` instead of `Tango` and use `await` with each API call: ```python +import os import asyncio from tango import AsyncTango client = AsyncTango( - client_id="My Client ID", - client_secret="My Client Secret", + access_token=os.environ.get("TANGO_API_ACCESS_TOKEN"), # This is the default and can be omitted ) @@ -79,10 +84,7 @@ All errors inherit from `tango.APIError`. import tango from tango import Tango -client = Tango( - client_id="My Client ID", - client_secret="My Client Secret", -) +client = Tango() try: client.schemas.retrieve() @@ -125,8 +127,6 @@ from tango import Tango client = Tango( # default is 2 max_retries=0, - client_id="My Client ID", - client_secret="My Client Secret", ) # Or, configure per-request: @@ -145,15 +145,11 @@ from tango import Tango client = Tango( # 20 seconds (default is 1 minute) timeout=20.0, - client_id="My Client ID", - client_secret="My Client Secret", ) # More granular control: client = Tango( timeout=httpx.Timeout(60.0, read=5.0, write=10.0, connect=2.0), - client_id="My Client ID", - client_secret="My Client Secret", ) # Override per-request: @@ -197,10 +193,7 @@ The "raw" Response object can be accessed by prefixing `.with_raw_response.` to ```py from tango import Tango -client = Tango( - client_id="My Client ID", - client_secret="My Client Secret", -) +client = Tango() response = client.schemas.with_raw_response.retrieve() print(response.headers.get('X-My-Header')) @@ -281,8 +274,6 @@ client = Tango( proxies="http://my.test.proxy.example.com", transport=httpx.HTTPTransport(local_address="0.0.0.0"), ), - client_id="My Client ID", - client_secret="My Client Secret", ) ``` diff --git a/pyproject.toml b/pyproject.toml index 68a70a8..244ba34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "tango-python" -version = "0.0.1-alpha.1" +version = "0.1.0-alpha.1" description = "The official Python library for the tango API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/tango/_client.py b/src/tango/_client.py index 7ffae74..a801e7a 100644 --- a/src/tango/_client.py +++ b/src/tango/_client.py @@ -66,14 +66,12 @@ class Tango(SyncAPIClient): with_streaming_response: TangoWithStreamedResponse # client options - client_id: str - client_secret: str + access_token: str def __init__( self, *, - client_id: str | None = None, - client_secret: str | None = None, + access_token: str | None = None, base_url: str | httpx.URL | None = None, timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, max_retries: int = DEFAULT_MAX_RETRIES, @@ -95,25 +93,15 @@ def __init__( ) -> None: """Construct a new synchronous tango client instance. - This automatically infers the following arguments from their corresponding environment variables if they are not provided: - - `client_id` from `CLIENT_ID` - - `client_secret` from `CLIENT_SECRET` + This automatically infers the `access_token` argument from the `TANGO_API_ACCESS_TOKEN` environment variable if it is not provided. """ - if client_id is None: - client_id = os.environ.get("CLIENT_ID") - if client_id is None: + if access_token is None: + access_token = os.environ.get("TANGO_API_ACCESS_TOKEN") + if access_token is None: raise TangoError( - "The client_id client option must be set either by passing client_id to the client or by setting the CLIENT_ID environment variable" + "The access_token client option must be set either by passing access_token to the client or by setting the TANGO_API_ACCESS_TOKEN environment variable" ) - self.client_id = client_id - - if client_secret is None: - client_secret = os.environ.get("CLIENT_SECRET") - if client_secret is None: - raise TangoError( - "The client_secret client option must be set either by passing client_secret to the client or by setting the CLIENT_SECRET environment variable" - ) - self.client_secret = client_secret + self.access_token = access_token if base_url is None: base_url = os.environ.get("TANGO_BASE_URL") @@ -155,6 +143,12 @@ def __init__( def qs(self) -> Querystring: return Querystring(array_format="comma") + @property + @override + def auth_headers(self) -> dict[str, str]: + access_token = self.access_token + return {"Authorization": f"Bearer {access_token}"} + @property @override def default_headers(self) -> dict[str, str | Omit]: @@ -167,8 +161,7 @@ def default_headers(self) -> dict[str, str | Omit]: def copy( self, *, - client_id: str | None = None, - client_secret: str | None = None, + access_token: str | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, http_client: httpx.Client | None = None, @@ -202,8 +195,7 @@ def copy( http_client = http_client or self._client return self.__class__( - client_id=client_id or self.client_id, - client_secret=client_secret or self.client_secret, + access_token=access_token or self.access_token, base_url=base_url or self.base_url, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, http_client=http_client, @@ -272,14 +264,12 @@ class AsyncTango(AsyncAPIClient): with_streaming_response: AsyncTangoWithStreamedResponse # client options - client_id: str - client_secret: str + access_token: str def __init__( self, *, - client_id: str | None = None, - client_secret: str | None = None, + access_token: str | None = None, base_url: str | httpx.URL | None = None, timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, max_retries: int = DEFAULT_MAX_RETRIES, @@ -301,25 +291,15 @@ def __init__( ) -> None: """Construct a new async tango client instance. - This automatically infers the following arguments from their corresponding environment variables if they are not provided: - - `client_id` from `CLIENT_ID` - - `client_secret` from `CLIENT_SECRET` + This automatically infers the `access_token` argument from the `TANGO_API_ACCESS_TOKEN` environment variable if it is not provided. """ - if client_id is None: - client_id = os.environ.get("CLIENT_ID") - if client_id is None: + if access_token is None: + access_token = os.environ.get("TANGO_API_ACCESS_TOKEN") + if access_token is None: raise TangoError( - "The client_id client option must be set either by passing client_id to the client or by setting the CLIENT_ID environment variable" + "The access_token client option must be set either by passing access_token to the client or by setting the TANGO_API_ACCESS_TOKEN environment variable" ) - self.client_id = client_id - - if client_secret is None: - client_secret = os.environ.get("CLIENT_SECRET") - if client_secret is None: - raise TangoError( - "The client_secret client option must be set either by passing client_secret to the client or by setting the CLIENT_SECRET environment variable" - ) - self.client_secret = client_secret + self.access_token = access_token if base_url is None: base_url = os.environ.get("TANGO_BASE_URL") @@ -361,6 +341,12 @@ def __init__( def qs(self) -> Querystring: return Querystring(array_format="comma") + @property + @override + def auth_headers(self) -> dict[str, str]: + access_token = self.access_token + return {"Authorization": f"Bearer {access_token}"} + @property @override def default_headers(self) -> dict[str, str | Omit]: @@ -373,8 +359,7 @@ def default_headers(self) -> dict[str, str | Omit]: def copy( self, *, - client_id: str | None = None, - client_secret: str | None = None, + access_token: str | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, http_client: httpx.AsyncClient | None = None, @@ -408,8 +393,7 @@ def copy( http_client = http_client or self._client return self.__class__( - client_id=client_id or self.client_id, - client_secret=client_secret or self.client_secret, + access_token=access_token or self.access_token, base_url=base_url or self.base_url, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, http_client=http_client, diff --git a/src/tango/_version.py b/src/tango/_version.py index 2c3d870..79abdde 100644 --- a/src/tango/_version.py +++ b/src/tango/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "tango" -__version__ = "0.0.1-alpha.1" # x-release-please-version +__version__ = "0.1.0-alpha.1" # x-release-please-version diff --git a/tests/conftest.py b/tests/conftest.py index 1824979..bddaa40 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -28,8 +28,7 @@ def pytest_collection_modifyitems(items: list[pytest.Function]) -> None: base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") -client_id = "My Client ID" -client_secret = "My Client Secret" +access_token = "My Access Token" @pytest.fixture(scope="session") @@ -38,9 +37,7 @@ def client(request: FixtureRequest) -> Iterator[Tango]: if not isinstance(strict, bool): raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") - with Tango( - base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=strict - ) as client: + with Tango(base_url=base_url, access_token=access_token, _strict_response_validation=strict) as client: yield client @@ -50,7 +47,5 @@ async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncTango]: if not isinstance(strict, bool): raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") - async with AsyncTango( - base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=strict - ) as client: + async with AsyncTango(base_url=base_url, access_token=access_token, _strict_response_validation=strict) as client: yield client diff --git a/tests/test_client.py b/tests/test_client.py index 5a92a10..c59b560 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -24,14 +24,13 @@ from tango._types import Omit from tango._models import BaseModel, FinalRequestOptions from tango._constants import RAW_RESPONSE_HEADER -from tango._exceptions import APIStatusError, APITimeoutError, APIResponseValidationError +from tango._exceptions import TangoError, APIStatusError, APITimeoutError, APIResponseValidationError from tango._base_client import DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, BaseClient, make_request_options from .utils import update_env base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") -client_id = "My Client ID" -client_secret = "My Client Secret" +access_token = "My Access Token" def _get_params(client: BaseClient[Any, Any]) -> dict[str, str]: @@ -53,9 +52,7 @@ def _get_open_connections(client: Tango | AsyncTango) -> int: class TestTango: - client = Tango( - base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=True - ) + client = Tango(base_url=base_url, access_token=access_token, _strict_response_validation=True) @pytest.mark.respx(base_url=base_url) def test_raw_response(self, respx_mock: MockRouter) -> None: @@ -81,13 +78,9 @@ def test_copy(self) -> None: copied = self.client.copy() assert id(copied) != id(self.client) - copied = self.client.copy(client_id="another My Client ID") - assert copied.client_id == "another My Client ID" - assert self.client.client_id == "My Client ID" - - copied = self.client.copy(client_secret="another My Client Secret") - assert copied.client_secret == "another My Client Secret" - assert self.client.client_secret == "My Client Secret" + copied = self.client.copy(access_token="another My Access Token") + assert copied.access_token == "another My Access Token" + assert self.client.access_token == "My Access Token" def test_copy_default_options(self) -> None: # options that have a default are overridden correctly @@ -108,8 +101,7 @@ def test_copy_default_options(self) -> None: def test_copy_default_headers(self) -> None: client = Tango( base_url=base_url, - client_id=client_id, - client_secret=client_secret, + access_token=access_token, _strict_response_validation=True, default_headers={"X-Foo": "bar"}, ) @@ -145,11 +137,7 @@ def test_copy_default_headers(self) -> None: def test_copy_default_query(self) -> None: client = Tango( - base_url=base_url, - client_id=client_id, - client_secret=client_secret, - _strict_response_validation=True, - default_query={"foo": "bar"}, + base_url=base_url, access_token=access_token, _strict_response_validation=True, default_query={"foo": "bar"} ) assert _get_params(client)["foo"] == "bar" @@ -274,11 +262,7 @@ def test_request_timeout(self) -> None: def test_client_timeout_option(self) -> None: client = Tango( - base_url=base_url, - client_id=client_id, - client_secret=client_secret, - _strict_response_validation=True, - timeout=httpx.Timeout(0), + base_url=base_url, access_token=access_token, _strict_response_validation=True, timeout=httpx.Timeout(0) ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -289,11 +273,7 @@ def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used with httpx.Client(timeout=None) as http_client: client = Tango( - base_url=base_url, - client_id=client_id, - client_secret=client_secret, - _strict_response_validation=True, - http_client=http_client, + base_url=base_url, access_token=access_token, _strict_response_validation=True, http_client=http_client ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -303,11 +283,7 @@ def test_http_client_timeout_option(self) -> None: # no timeout given to the httpx client should not use the httpx default with httpx.Client() as http_client: client = Tango( - base_url=base_url, - client_id=client_id, - client_secret=client_secret, - _strict_response_validation=True, - http_client=http_client, + base_url=base_url, access_token=access_token, _strict_response_validation=True, http_client=http_client ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -317,11 +293,7 @@ def test_http_client_timeout_option(self) -> None: # explicitly passing the default timeout currently results in it being ignored with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: client = Tango( - base_url=base_url, - client_id=client_id, - client_secret=client_secret, - _strict_response_validation=True, - http_client=http_client, + base_url=base_url, access_token=access_token, _strict_response_validation=True, http_client=http_client ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -333,8 +305,7 @@ async def test_invalid_http_client(self) -> None: async with httpx.AsyncClient() as http_client: Tango( base_url=base_url, - client_id=client_id, - client_secret=client_secret, + access_token=access_token, _strict_response_validation=True, http_client=cast(Any, http_client), ) @@ -342,8 +313,7 @@ async def test_invalid_http_client(self) -> None: def test_default_headers_option(self) -> None: client = Tango( base_url=base_url, - client_id=client_id, - client_secret=client_secret, + access_token=access_token, _strict_response_validation=True, default_headers={"X-Foo": "bar"}, ) @@ -353,8 +323,7 @@ def test_default_headers_option(self) -> None: client2 = Tango( base_url=base_url, - client_id=client_id, - client_secret=client_secret, + access_token=access_token, _strict_response_validation=True, default_headers={ "X-Foo": "stainless", @@ -365,11 +334,20 @@ def test_default_headers_option(self) -> None: assert request.headers.get("x-foo") == "stainless" assert request.headers.get("x-stainless-lang") == "my-overriding-header" + def test_validate_headers(self) -> None: + client = Tango(base_url=base_url, access_token=access_token, _strict_response_validation=True) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("Authorization") == f"Bearer {access_token}" + + with pytest.raises(TangoError): + with update_env(**{"TANGO_API_ACCESS_TOKEN": Omit()}): + client2 = Tango(base_url=base_url, access_token=None, _strict_response_validation=True) + _ = client2 + def test_default_query_option(self) -> None: client = Tango( base_url=base_url, - client_id=client_id, - client_secret=client_secret, + access_token=access_token, _strict_response_validation=True, default_query={"query_param": "bar"}, ) @@ -572,10 +550,7 @@ class Model(BaseModel): def test_base_url_setter(self) -> None: client = Tango( - base_url="https://example.com/from_init", - client_id=client_id, - client_secret=client_secret, - _strict_response_validation=True, + base_url="https://example.com/from_init", access_token=access_token, _strict_response_validation=True ) assert client.base_url == "https://example.com/from_init/" @@ -585,7 +560,7 @@ def test_base_url_setter(self) -> None: def test_base_url_env(self) -> None: with update_env(TANGO_BASE_URL="http://localhost:5000/from/env"): - client = Tango(client_id=client_id, client_secret=client_secret, _strict_response_validation=True) + client = Tango(access_token=access_token, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" @pytest.mark.parametrize( @@ -593,14 +568,12 @@ def test_base_url_env(self) -> None: [ Tango( base_url="http://localhost:5000/custom/path/", - client_id=client_id, - client_secret=client_secret, + access_token=access_token, _strict_response_validation=True, ), Tango( base_url="http://localhost:5000/custom/path/", - client_id=client_id, - client_secret=client_secret, + access_token=access_token, _strict_response_validation=True, http_client=httpx.Client(), ), @@ -622,14 +595,12 @@ def test_base_url_trailing_slash(self, client: Tango) -> None: [ Tango( base_url="http://localhost:5000/custom/path/", - client_id=client_id, - client_secret=client_secret, + access_token=access_token, _strict_response_validation=True, ), Tango( base_url="http://localhost:5000/custom/path/", - client_id=client_id, - client_secret=client_secret, + access_token=access_token, _strict_response_validation=True, http_client=httpx.Client(), ), @@ -651,14 +622,12 @@ def test_base_url_no_trailing_slash(self, client: Tango) -> None: [ Tango( base_url="http://localhost:5000/custom/path/", - client_id=client_id, - client_secret=client_secret, + access_token=access_token, _strict_response_validation=True, ), Tango( base_url="http://localhost:5000/custom/path/", - client_id=client_id, - client_secret=client_secret, + access_token=access_token, _strict_response_validation=True, http_client=httpx.Client(), ), @@ -676,9 +645,7 @@ def test_absolute_request_url(self, client: Tango) -> None: assert request.url == "https://myapi.com/foo" def test_copied_client_does_not_close_http(self) -> None: - client = Tango( - base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=True - ) + client = Tango(base_url=base_url, access_token=access_token, _strict_response_validation=True) assert not client.is_closed() copied = client.copy() @@ -689,9 +656,7 @@ def test_copied_client_does_not_close_http(self) -> None: assert not client.is_closed() def test_client_context_manager(self) -> None: - client = Tango( - base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=True - ) + client = Tango(base_url=base_url, access_token=access_token, _strict_response_validation=True) with client as c2: assert c2 is client assert not c2.is_closed() @@ -714,8 +679,7 @@ def test_client_max_retries_validation(self) -> None: with pytest.raises(TypeError, match=r"max_retries cannot be None"): Tango( base_url=base_url, - client_id=client_id, - client_secret=client_secret, + access_token=access_token, _strict_response_validation=True, max_retries=cast(Any, None), ) @@ -727,16 +691,12 @@ class Model(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) - strict_client = Tango( - base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=True - ) + strict_client = Tango(base_url=base_url, access_token=access_token, _strict_response_validation=True) with pytest.raises(APIResponseValidationError): strict_client.get("/foo", cast_to=Model) - client = Tango( - base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=False - ) + client = Tango(base_url=base_url, access_token=access_token, _strict_response_validation=False) response = client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] @@ -764,9 +724,7 @@ class Model(BaseModel): ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: - client = Tango( - base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=True - ) + client = Tango(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=3) @@ -874,9 +832,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: class TestAsyncTango: - client = AsyncTango( - base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=True - ) + client = AsyncTango(base_url=base_url, access_token=access_token, _strict_response_validation=True) @pytest.mark.respx(base_url=base_url) @pytest.mark.asyncio @@ -904,13 +860,9 @@ def test_copy(self) -> None: copied = self.client.copy() assert id(copied) != id(self.client) - copied = self.client.copy(client_id="another My Client ID") - assert copied.client_id == "another My Client ID" - assert self.client.client_id == "My Client ID" - - copied = self.client.copy(client_secret="another My Client Secret") - assert copied.client_secret == "another My Client Secret" - assert self.client.client_secret == "My Client Secret" + copied = self.client.copy(access_token="another My Access Token") + assert copied.access_token == "another My Access Token" + assert self.client.access_token == "My Access Token" def test_copy_default_options(self) -> None: # options that have a default are overridden correctly @@ -931,8 +883,7 @@ def test_copy_default_options(self) -> None: def test_copy_default_headers(self) -> None: client = AsyncTango( base_url=base_url, - client_id=client_id, - client_secret=client_secret, + access_token=access_token, _strict_response_validation=True, default_headers={"X-Foo": "bar"}, ) @@ -968,11 +919,7 @@ def test_copy_default_headers(self) -> None: def test_copy_default_query(self) -> None: client = AsyncTango( - base_url=base_url, - client_id=client_id, - client_secret=client_secret, - _strict_response_validation=True, - default_query={"foo": "bar"}, + base_url=base_url, access_token=access_token, _strict_response_validation=True, default_query={"foo": "bar"} ) assert _get_params(client)["foo"] == "bar" @@ -1097,11 +1044,7 @@ async def test_request_timeout(self) -> None: async def test_client_timeout_option(self) -> None: client = AsyncTango( - base_url=base_url, - client_id=client_id, - client_secret=client_secret, - _strict_response_validation=True, - timeout=httpx.Timeout(0), + base_url=base_url, access_token=access_token, _strict_response_validation=True, timeout=httpx.Timeout(0) ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1112,11 +1055,7 @@ async def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used async with httpx.AsyncClient(timeout=None) as http_client: client = AsyncTango( - base_url=base_url, - client_id=client_id, - client_secret=client_secret, - _strict_response_validation=True, - http_client=http_client, + base_url=base_url, access_token=access_token, _strict_response_validation=True, http_client=http_client ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1126,11 +1065,7 @@ async def test_http_client_timeout_option(self) -> None: # no timeout given to the httpx client should not use the httpx default async with httpx.AsyncClient() as http_client: client = AsyncTango( - base_url=base_url, - client_id=client_id, - client_secret=client_secret, - _strict_response_validation=True, - http_client=http_client, + base_url=base_url, access_token=access_token, _strict_response_validation=True, http_client=http_client ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1140,11 +1075,7 @@ async def test_http_client_timeout_option(self) -> None: # explicitly passing the default timeout currently results in it being ignored async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: client = AsyncTango( - base_url=base_url, - client_id=client_id, - client_secret=client_secret, - _strict_response_validation=True, - http_client=http_client, + base_url=base_url, access_token=access_token, _strict_response_validation=True, http_client=http_client ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1156,8 +1087,7 @@ def test_invalid_http_client(self) -> None: with httpx.Client() as http_client: AsyncTango( base_url=base_url, - client_id=client_id, - client_secret=client_secret, + access_token=access_token, _strict_response_validation=True, http_client=cast(Any, http_client), ) @@ -1165,8 +1095,7 @@ def test_invalid_http_client(self) -> None: def test_default_headers_option(self) -> None: client = AsyncTango( base_url=base_url, - client_id=client_id, - client_secret=client_secret, + access_token=access_token, _strict_response_validation=True, default_headers={"X-Foo": "bar"}, ) @@ -1176,8 +1105,7 @@ def test_default_headers_option(self) -> None: client2 = AsyncTango( base_url=base_url, - client_id=client_id, - client_secret=client_secret, + access_token=access_token, _strict_response_validation=True, default_headers={ "X-Foo": "stainless", @@ -1188,11 +1116,20 @@ def test_default_headers_option(self) -> None: assert request.headers.get("x-foo") == "stainless" assert request.headers.get("x-stainless-lang") == "my-overriding-header" + def test_validate_headers(self) -> None: + client = AsyncTango(base_url=base_url, access_token=access_token, _strict_response_validation=True) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("Authorization") == f"Bearer {access_token}" + + with pytest.raises(TangoError): + with update_env(**{"TANGO_API_ACCESS_TOKEN": Omit()}): + client2 = AsyncTango(base_url=base_url, access_token=None, _strict_response_validation=True) + _ = client2 + def test_default_query_option(self) -> None: client = AsyncTango( base_url=base_url, - client_id=client_id, - client_secret=client_secret, + access_token=access_token, _strict_response_validation=True, default_query={"query_param": "bar"}, ) @@ -1395,10 +1332,7 @@ class Model(BaseModel): def test_base_url_setter(self) -> None: client = AsyncTango( - base_url="https://example.com/from_init", - client_id=client_id, - client_secret=client_secret, - _strict_response_validation=True, + base_url="https://example.com/from_init", access_token=access_token, _strict_response_validation=True ) assert client.base_url == "https://example.com/from_init/" @@ -1408,7 +1342,7 @@ def test_base_url_setter(self) -> None: def test_base_url_env(self) -> None: with update_env(TANGO_BASE_URL="http://localhost:5000/from/env"): - client = AsyncTango(client_id=client_id, client_secret=client_secret, _strict_response_validation=True) + client = AsyncTango(access_token=access_token, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" @pytest.mark.parametrize( @@ -1416,14 +1350,12 @@ def test_base_url_env(self) -> None: [ AsyncTango( base_url="http://localhost:5000/custom/path/", - client_id=client_id, - client_secret=client_secret, + access_token=access_token, _strict_response_validation=True, ), AsyncTango( base_url="http://localhost:5000/custom/path/", - client_id=client_id, - client_secret=client_secret, + access_token=access_token, _strict_response_validation=True, http_client=httpx.AsyncClient(), ), @@ -1445,14 +1377,12 @@ def test_base_url_trailing_slash(self, client: AsyncTango) -> None: [ AsyncTango( base_url="http://localhost:5000/custom/path/", - client_id=client_id, - client_secret=client_secret, + access_token=access_token, _strict_response_validation=True, ), AsyncTango( base_url="http://localhost:5000/custom/path/", - client_id=client_id, - client_secret=client_secret, + access_token=access_token, _strict_response_validation=True, http_client=httpx.AsyncClient(), ), @@ -1474,14 +1404,12 @@ def test_base_url_no_trailing_slash(self, client: AsyncTango) -> None: [ AsyncTango( base_url="http://localhost:5000/custom/path/", - client_id=client_id, - client_secret=client_secret, + access_token=access_token, _strict_response_validation=True, ), AsyncTango( base_url="http://localhost:5000/custom/path/", - client_id=client_id, - client_secret=client_secret, + access_token=access_token, _strict_response_validation=True, http_client=httpx.AsyncClient(), ), @@ -1499,9 +1427,7 @@ def test_absolute_request_url(self, client: AsyncTango) -> None: assert request.url == "https://myapi.com/foo" async def test_copied_client_does_not_close_http(self) -> None: - client = AsyncTango( - base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=True - ) + client = AsyncTango(base_url=base_url, access_token=access_token, _strict_response_validation=True) assert not client.is_closed() copied = client.copy() @@ -1513,9 +1439,7 @@ async def test_copied_client_does_not_close_http(self) -> None: assert not client.is_closed() async def test_client_context_manager(self) -> None: - client = AsyncTango( - base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=True - ) + client = AsyncTango(base_url=base_url, access_token=access_token, _strict_response_validation=True) async with client as c2: assert c2 is client assert not c2.is_closed() @@ -1539,8 +1463,7 @@ async def test_client_max_retries_validation(self) -> None: with pytest.raises(TypeError, match=r"max_retries cannot be None"): AsyncTango( base_url=base_url, - client_id=client_id, - client_secret=client_secret, + access_token=access_token, _strict_response_validation=True, max_retries=cast(Any, None), ) @@ -1553,16 +1476,12 @@ class Model(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) - strict_client = AsyncTango( - base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=True - ) + strict_client = AsyncTango(base_url=base_url, access_token=access_token, _strict_response_validation=True) with pytest.raises(APIResponseValidationError): await strict_client.get("/foo", cast_to=Model) - client = AsyncTango( - base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=False - ) + client = AsyncTango(base_url=base_url, access_token=access_token, _strict_response_validation=False) response = await client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] @@ -1591,9 +1510,7 @@ class Model(BaseModel): @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) @pytest.mark.asyncio async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: - client = AsyncTango( - base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=True - ) + client = AsyncTango(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=3)