diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2437b419..bf0d0361 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.4.0" + ".": "3.5.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 3f41901a..af483f0a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 175 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/digitalocean%2Fgradient-d15c113822740c8a5cd0d054d186c524ea6e15a9e64e8d0662b5a5a667745aaa.yml -openapi_spec_hash: b56f95892c05800b022f39d77087037b -config_hash: 8497af1695ff361853c745dd869dc6b9 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/digitalocean%2Fgradient-cb3bf9b21459cad24410206c27a32fd31ef6cf86711700597549dbbd0d634002.yml +openapi_spec_hash: 6a9149a81ba15e7c5c5c1f4d77daad92 +config_hash: bad49c3bf949d5168ec3896bedff253a diff --git a/CHANGELOG.md b/CHANGELOG.md index c39b7324..d8e8b1d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 3.5.0 (2025-10-14) + +Full Changelog: [v3.4.0...v3.5.0](https://github.com/digitalocean/gradient-python/compare/v3.4.0...v3.5.0) + +### Features + +* **api:** update via SDK Studio ([#74](https://github.com/digitalocean/gradient-python/issues/74)) ([e1ab040](https://github.com/digitalocean/gradient-python/commit/e1ab0407e88f5394f5c299940a4b2fe72dbbf70e)) + + +### Chores + +* **internal:** detect missing future annotations with ruff ([0fb9f92](https://github.com/digitalocean/gradient-python/commit/0fb9f9254a0f72a721fa73823399e58eec723f1a)) + ## 3.4.0 (2025-10-09) Full Changelog: [v3.3.0...v3.4.0](https://github.com/digitalocean/gradient-python/compare/v3.3.0...v3.4.0) diff --git a/README.md b/README.md index 30b7c75a..c9186c03 100644 --- a/README.md +++ b/README.md @@ -44,12 +44,14 @@ client = Gradient( ), # This is the default and can be omitted ) inference_client = Gradient( - inference_key=os.environ.get( + model_access_key=os.environ.get( "GRADIENT_MODEL_ACCESS_KEY" ), # This is the default and can be omitted ) agent_client = Gradient( - agent_key=os.environ.get("GRADIENT_AGENT_ACCESS_KEY"), # This is the default and can be omitted + agent_access_key=os.environ.get( + "GRADIENT_AGENT_ACCESS_KEY" + ), # This is the default and can be omitted agent_endpoint="https://my-agent.agents.do-ai.run", ) @@ -103,11 +105,7 @@ import os import asyncio from gradient import AsyncGradient -client = AsyncGradient( - access_token=os.environ.get( - "DIGITALOCEAN_ACCESS_TOKEN" - ), # This is the default and can be omitted -) +client = AsyncGradient() async def main() -> None: @@ -149,7 +147,6 @@ from gradient import AsyncGradient async def main() -> None: async with AsyncGradient( - access_token="My Access Token", http_client=DefaultAioHttpClient(), ) as client: completion = await client.chat.completions.create( diff --git a/pyproject.toml b/pyproject.toml index 75cb7f7b..b9dbc5ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "gradient" -version = "3.4.0" +version = "3.5.0" description = "The official Python library for the Gradient API" dynamic = ["readme"] license = "Apache-2.0" @@ -211,6 +211,8 @@ select = [ "B", # remove unused imports "F401", + # check for missing future annotations + "FA102", # bare except statements "E722", # unused arguments @@ -233,6 +235,8 @@ unfixable = [ "T203", ] +extend-safe-fixes = ["FA102"] + [tool.ruff.lint.flake8-tidy-imports.banned-api] "functools.lru_cache".msg = "This function does not retain type information for the wrapped function's arguments; The `lru_cache` function from `_utils` should be used instead" diff --git a/src/gradient/_base_client.py b/src/gradient/_base_client.py index 89146b71..c6d52884 100644 --- a/src/gradient/_base_client.py +++ b/src/gradient/_base_client.py @@ -548,14 +548,14 @@ def _build_request( # TODO: report this error to httpx return self._client.build_request( # pyright: ignore[reportUnknownMemberType] headers=headers, - timeout=self.timeout if isinstance(options.timeout, NotGiven) else options.timeout, + timeout=(self.timeout if isinstance(options.timeout, NotGiven) else options.timeout), method=options.method, url=prepared_url, # the `Query` type that we use is incompatible with qs' # `Params` type as it needs to be typed as `Mapping[str, object]` # so that passing a `TypedDict` doesn't cause an error. # https://github.com/microsoft/pyright/issues/3526#event-6715453066 - params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None, + params=(self.qs.stringify(cast(Mapping[str, Any], params)) if params else None), **kwargs, ) @@ -1067,7 +1067,12 @@ def request( ) def _sleep_for_retry( - self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None + self, + *, + retries_taken: int, + max_retries: int, + options: FinalRequestOptions, + response: httpx.Response | None, ) -> None: remaining_retries = max_retries - retries_taken if remaining_retries == 1: @@ -1248,7 +1253,11 @@ def post( stream_cls: type[_StreamT] | None = None, ) -> ResponseT | _StreamT: opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=to_httpx_files(files), **options + method="post", + url=path, + json_data=body, + files=to_httpx_files(files), + **options, ) return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) @@ -1273,7 +1282,11 @@ def put( options: RequestOptions = {}, ) -> ResponseT: opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=to_httpx_files(files), **options + method="put", + url=path, + json_data=body, + files=to_httpx_files(files), + **options, ) return self.request(cast_to, opts) @@ -1317,6 +1330,7 @@ def __init__(self, **kwargs: Any) -> None: class _DefaultAioHttpClient(httpx.AsyncClient): def __init__(self, **_kwargs: Any) -> None: raise RuntimeError("To use the aiohttp client you must have installed the package with the `aiohttp` extra") + else: class _DefaultAioHttpClient(httpx_aiohttp.HttpxAiohttpClient): # type: ignore @@ -1603,7 +1617,12 @@ async def request( ) async def _sleep_for_retry( - self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None + self, + *, + retries_taken: int, + max_retries: int, + options: FinalRequestOptions, + response: httpx.Response | None, ) -> None: remaining_retries = max_retries - retries_taken if remaining_retries == 1: @@ -1772,7 +1791,11 @@ async def post( stream_cls: type[_AsyncStreamT] | None = None, ) -> ResponseT | _AsyncStreamT: opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="post", + url=path, + json_data=body, + files=await async_to_httpx_files(files), + **options, ) return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) @@ -1797,7 +1820,11 @@ async def put( options: RequestOptions = {}, ) -> ResponseT: opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="put", + url=path, + json_data=body, + files=await async_to_httpx_files(files), + **options, ) return await self.request(cast_to, opts) diff --git a/src/gradient/_client.py b/src/gradient/_client.py index 4f6c3796..77cc446e 100644 --- a/src/gradient/_client.py +++ b/src/gradient/_client.py @@ -242,11 +242,29 @@ def qs(self) -> Querystring: @property @override def auth_headers(self) -> dict[str, str]: + return {**self._model_access_key, **self._agent_access_key, **self._bearer_auth} + + @property + def _bearer_auth(self) -> dict[str, str]: access_token = self.access_token if access_token is None: return {} return {"Authorization": f"Bearer {access_token}"} + @property + def _model_access_key(self) -> dict[str, str]: + model_access_key = self.model_access_key + if model_access_key is None: + return {} + return {"Authorization": f"Bearer {model_access_key}"} + + @property + def _agent_access_key(self) -> dict[str, str]: + agent_access_key = self.agent_access_key + if agent_access_key is None: + return {} + return {"Authorization": f"Bearer {agent_access_key}"} + @property @override def default_headers(self) -> dict[str, str | Omit]: @@ -263,6 +281,16 @@ def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None: if isinstance(custom_headers.get("Authorization"), Omit): return + if self.model_access_key and headers.get("Authorization"): + return + if isinstance(custom_headers.get("Authorization"), Omit): + return + + if self.agent_access_key and headers.get("Authorization"): + return + if isinstance(custom_headers.get("Authorization"), Omit): + return + raise TypeError( '"Could not resolve authentication method. Expected access_token, agent_access_key, or model_access_key to be set. Or for the `Authorization` headers to be explicitly omitted"' ) @@ -537,11 +565,29 @@ def qs(self) -> Querystring: @property @override def auth_headers(self) -> dict[str, str]: + return {**self._model_access_key, **self._agent_access_key, **self._bearer_auth} + + @property + def _bearer_auth(self) -> dict[str, str]: access_token = self.access_token if access_token is None: return {} return {"Authorization": f"Bearer {access_token}"} + @property + def _model_access_key(self) -> dict[str, str]: + model_access_key = self.model_access_key + if model_access_key is None: + return {} + return {"Authorization": f"Bearer {model_access_key}"} + + @property + def _agent_access_key(self) -> dict[str, str]: + agent_access_key = self.agent_access_key + if agent_access_key is None: + return {} + return {"Authorization": f"Bearer {agent_access_key}"} + @property @override def default_headers(self) -> dict[str, str | Omit]: @@ -558,6 +604,16 @@ def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None: if isinstance(custom_headers.get("Authorization"), Omit): return + if self.model_access_key and headers.get("Authorization"): + return + if isinstance(custom_headers.get("Authorization"), Omit): + return + + if self.agent_access_key and headers.get("Authorization"): + return + if isinstance(custom_headers.get("Authorization"), Omit): + return + raise TypeError( '"Could not resolve authentication method. Expected access_token, agent_access_key, or model_access_key to be set. Or for the `Authorization` headers to be explicitly omitted"' ) diff --git a/src/gradient/_version.py b/src/gradient/_version.py index 440935e9..68c981bc 100644 --- a/src/gradient/_version.py +++ b/src/gradient/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "gradient" -__version__ = "3.4.0" # x-release-please-version +__version__ = "3.5.0" # x-release-please-version diff --git a/tests/test_client.py b/tests/test_client.py index 98833ff2..ddf1c4db 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -414,12 +414,14 @@ def test_validate_headers(self) -> None: request = client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("Authorization") == f"Bearer {access_token}" - with update_env(**{"DIGITALOCEAN_ACCESS_TOKEN": Omit()}): + with update_env( + **{"DIGITALOCEAN_ACCESS_TOKEN": Omit(), "MODEL_ACCESS_KEY": Omit(), "AGENT_ACCESS_KEY": Omit()} + ): client2 = Gradient( base_url=base_url, access_token=None, - model_access_key=model_access_key, - agent_access_key=agent_access_key, + model_access_key=None, + agent_access_key=None, _strict_response_validation=True, ) @@ -1429,12 +1431,14 @@ def test_validate_headers(self) -> None: request = client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("Authorization") == f"Bearer {access_token}" - with update_env(**{"DIGITALOCEAN_ACCESS_TOKEN": Omit()}): + with update_env( + **{"DIGITALOCEAN_ACCESS_TOKEN": Omit(), "MODEL_ACCESS_KEY": Omit(), "AGENT_ACCESS_KEY": Omit()} + ): client2 = AsyncGradient( base_url=base_url, access_token=None, - model_access_key=model_access_key, - agent_access_key=agent_access_key, + model_access_key=None, + agent_access_key=None, _strict_response_validation=True, )