Skip to content
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
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "3.4.0"
".": "3.5.0"
}
6 changes: 3 additions & 3 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
13 changes: 5 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand Down
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -211,6 +211,8 @@ select = [
"B",
# remove unused imports
"F401",
# check for missing future annotations
"FA102",
# bare except statements
"E722",
# unused arguments
Expand All @@ -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"

Expand Down
43 changes: 35 additions & 8 deletions src/gradient/_base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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))

Expand All @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Expand All @@ -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)

Expand Down
56 changes: 56 additions & 0 deletions src/gradient/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand All @@ -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"'
)
Expand Down Expand Up @@ -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]:
Expand All @@ -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"'
)
Expand Down
2 changes: 1 addition & 1 deletion src/gradient/_version.py
Original file line number Diff line number Diff line change
@@ -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
16 changes: 10 additions & 6 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down Expand Up @@ -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,
)

Expand Down