Skip to content

Commit 292da2b

Browse files
release: 3.5.0 (#55)
* chore(internal): detect missing future annotations with ruff * feat(api): update via SDK Studio (#74) * release: 3.5.0 --------- Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com> Co-authored-by: Ben Batha <bbatha@digitalocean.com>
1 parent 0dccc0c commit 292da2b

File tree

9 files changed

+129
-28
lines changed

9 files changed

+129
-28
lines changed

.release-please-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "3.4.0"
2+
".": "3.5.0"
33
}

.stats.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
configured_endpoints: 175
2-
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/digitalocean%2Fgradient-d15c113822740c8a5cd0d054d186c524ea6e15a9e64e8d0662b5a5a667745aaa.yml
3-
openapi_spec_hash: b56f95892c05800b022f39d77087037b
4-
config_hash: 8497af1695ff361853c745dd869dc6b9
2+
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/digitalocean%2Fgradient-cb3bf9b21459cad24410206c27a32fd31ef6cf86711700597549dbbd0d634002.yml
3+
openapi_spec_hash: 6a9149a81ba15e7c5c5c1f4d77daad92
4+
config_hash: bad49c3bf949d5168ec3896bedff253a

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

3+
## 3.5.0 (2025-10-14)
4+
5+
Full Changelog: [v3.4.0...v3.5.0](https://github.com/digitalocean/gradient-python/compare/v3.4.0...v3.5.0)
6+
7+
### Features
8+
9+
* **api:** update via SDK Studio ([#74](https://github.com/digitalocean/gradient-python/issues/74)) ([e1ab040](https://github.com/digitalocean/gradient-python/commit/e1ab0407e88f5394f5c299940a4b2fe72dbbf70e))
10+
11+
12+
### Chores
13+
14+
* **internal:** detect missing future annotations with ruff ([0fb9f92](https://github.com/digitalocean/gradient-python/commit/0fb9f9254a0f72a721fa73823399e58eec723f1a))
15+
316
## 3.4.0 (2025-10-09)
417

518
Full Changelog: [v3.3.0...v3.4.0](https://github.com/digitalocean/gradient-python/compare/v3.3.0...v3.4.0)

README.md

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,14 @@ client = Gradient(
4444
), # This is the default and can be omitted
4545
)
4646
inference_client = Gradient(
47-
inference_key=os.environ.get(
47+
model_access_key=os.environ.get(
4848
"GRADIENT_MODEL_ACCESS_KEY"
4949
), # This is the default and can be omitted
5050
)
5151
agent_client = Gradient(
52-
agent_key=os.environ.get("GRADIENT_AGENT_ACCESS_KEY"), # This is the default and can be omitted
52+
agent_access_key=os.environ.get(
53+
"GRADIENT_AGENT_ACCESS_KEY"
54+
), # This is the default and can be omitted
5355
agent_endpoint="https://my-agent.agents.do-ai.run",
5456
)
5557

@@ -103,11 +105,7 @@ import os
103105
import asyncio
104106
from gradient import AsyncGradient
105107

106-
client = AsyncGradient(
107-
access_token=os.environ.get(
108-
"DIGITALOCEAN_ACCESS_TOKEN"
109-
), # This is the default and can be omitted
110-
)
108+
client = AsyncGradient()
111109

112110

113111
async def main() -> None:
@@ -149,7 +147,6 @@ from gradient import AsyncGradient
149147

150148
async def main() -> None:
151149
async with AsyncGradient(
152-
access_token="My Access Token",
153150
http_client=DefaultAioHttpClient(),
154151
) as client:
155152
completion = await client.chat.completions.create(

pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "gradient"
3-
version = "3.4.0"
3+
version = "3.5.0"
44
description = "The official Python library for the Gradient API"
55
dynamic = ["readme"]
66
license = "Apache-2.0"
@@ -211,6 +211,8 @@ select = [
211211
"B",
212212
# remove unused imports
213213
"F401",
214+
# check for missing future annotations
215+
"FA102",
214216
# bare except statements
215217
"E722",
216218
# unused arguments
@@ -233,6 +235,8 @@ unfixable = [
233235
"T203",
234236
]
235237

238+
extend-safe-fixes = ["FA102"]
239+
236240
[tool.ruff.lint.flake8-tidy-imports.banned-api]
237241
"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"
238242

src/gradient/_base_client.py

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -548,14 +548,14 @@ def _build_request(
548548
# TODO: report this error to httpx
549549
return self._client.build_request( # pyright: ignore[reportUnknownMemberType]
550550
headers=headers,
551-
timeout=self.timeout if isinstance(options.timeout, NotGiven) else options.timeout,
551+
timeout=(self.timeout if isinstance(options.timeout, NotGiven) else options.timeout),
552552
method=options.method,
553553
url=prepared_url,
554554
# the `Query` type that we use is incompatible with qs'
555555
# `Params` type as it needs to be typed as `Mapping[str, object]`
556556
# so that passing a `TypedDict` doesn't cause an error.
557557
# https://github.com/microsoft/pyright/issues/3526#event-6715453066
558-
params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None,
558+
params=(self.qs.stringify(cast(Mapping[str, Any], params)) if params else None),
559559
**kwargs,
560560
)
561561

@@ -1067,7 +1067,12 @@ def request(
10671067
)
10681068

10691069
def _sleep_for_retry(
1070-
self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None
1070+
self,
1071+
*,
1072+
retries_taken: int,
1073+
max_retries: int,
1074+
options: FinalRequestOptions,
1075+
response: httpx.Response | None,
10711076
) -> None:
10721077
remaining_retries = max_retries - retries_taken
10731078
if remaining_retries == 1:
@@ -1248,7 +1253,11 @@ def post(
12481253
stream_cls: type[_StreamT] | None = None,
12491254
) -> ResponseT | _StreamT:
12501255
opts = FinalRequestOptions.construct(
1251-
method="post", url=path, json_data=body, files=to_httpx_files(files), **options
1256+
method="post",
1257+
url=path,
1258+
json_data=body,
1259+
files=to_httpx_files(files),
1260+
**options,
12521261
)
12531262
return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))
12541263

@@ -1273,7 +1282,11 @@ def put(
12731282
options: RequestOptions = {},
12741283
) -> ResponseT:
12751284
opts = FinalRequestOptions.construct(
1276-
method="put", url=path, json_data=body, files=to_httpx_files(files), **options
1285+
method="put",
1286+
url=path,
1287+
json_data=body,
1288+
files=to_httpx_files(files),
1289+
**options,
12771290
)
12781291
return self.request(cast_to, opts)
12791292

@@ -1317,6 +1330,7 @@ def __init__(self, **kwargs: Any) -> None:
13171330
class _DefaultAioHttpClient(httpx.AsyncClient):
13181331
def __init__(self, **_kwargs: Any) -> None:
13191332
raise RuntimeError("To use the aiohttp client you must have installed the package with the `aiohttp` extra")
1333+
13201334
else:
13211335

13221336
class _DefaultAioHttpClient(httpx_aiohttp.HttpxAiohttpClient): # type: ignore
@@ -1603,7 +1617,12 @@ async def request(
16031617
)
16041618

16051619
async def _sleep_for_retry(
1606-
self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None
1620+
self,
1621+
*,
1622+
retries_taken: int,
1623+
max_retries: int,
1624+
options: FinalRequestOptions,
1625+
response: httpx.Response | None,
16071626
) -> None:
16081627
remaining_retries = max_retries - retries_taken
16091628
if remaining_retries == 1:
@@ -1772,7 +1791,11 @@ async def post(
17721791
stream_cls: type[_AsyncStreamT] | None = None,
17731792
) -> ResponseT | _AsyncStreamT:
17741793
opts = FinalRequestOptions.construct(
1775-
method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options
1794+
method="post",
1795+
url=path,
1796+
json_data=body,
1797+
files=await async_to_httpx_files(files),
1798+
**options,
17761799
)
17771800
return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)
17781801

@@ -1797,7 +1820,11 @@ async def put(
17971820
options: RequestOptions = {},
17981821
) -> ResponseT:
17991822
opts = FinalRequestOptions.construct(
1800-
method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options
1823+
method="put",
1824+
url=path,
1825+
json_data=body,
1826+
files=await async_to_httpx_files(files),
1827+
**options,
18011828
)
18021829
return await self.request(cast_to, opts)
18031830

src/gradient/_client.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,11 +242,29 @@ def qs(self) -> Querystring:
242242
@property
243243
@override
244244
def auth_headers(self) -> dict[str, str]:
245+
return {**self._model_access_key, **self._agent_access_key, **self._bearer_auth}
246+
247+
@property
248+
def _bearer_auth(self) -> dict[str, str]:
245249
access_token = self.access_token
246250
if access_token is None:
247251
return {}
248252
return {"Authorization": f"Bearer {access_token}"}
249253

254+
@property
255+
def _model_access_key(self) -> dict[str, str]:
256+
model_access_key = self.model_access_key
257+
if model_access_key is None:
258+
return {}
259+
return {"Authorization": f"Bearer {model_access_key}"}
260+
261+
@property
262+
def _agent_access_key(self) -> dict[str, str]:
263+
agent_access_key = self.agent_access_key
264+
if agent_access_key is None:
265+
return {}
266+
return {"Authorization": f"Bearer {agent_access_key}"}
267+
250268
@property
251269
@override
252270
def default_headers(self) -> dict[str, str | Omit]:
@@ -263,6 +281,16 @@ def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None:
263281
if isinstance(custom_headers.get("Authorization"), Omit):
264282
return
265283

284+
if self.model_access_key and headers.get("Authorization"):
285+
return
286+
if isinstance(custom_headers.get("Authorization"), Omit):
287+
return
288+
289+
if self.agent_access_key and headers.get("Authorization"):
290+
return
291+
if isinstance(custom_headers.get("Authorization"), Omit):
292+
return
293+
266294
raise TypeError(
267295
'"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"'
268296
)
@@ -537,11 +565,29 @@ def qs(self) -> Querystring:
537565
@property
538566
@override
539567
def auth_headers(self) -> dict[str, str]:
568+
return {**self._model_access_key, **self._agent_access_key, **self._bearer_auth}
569+
570+
@property
571+
def _bearer_auth(self) -> dict[str, str]:
540572
access_token = self.access_token
541573
if access_token is None:
542574
return {}
543575
return {"Authorization": f"Bearer {access_token}"}
544576

577+
@property
578+
def _model_access_key(self) -> dict[str, str]:
579+
model_access_key = self.model_access_key
580+
if model_access_key is None:
581+
return {}
582+
return {"Authorization": f"Bearer {model_access_key}"}
583+
584+
@property
585+
def _agent_access_key(self) -> dict[str, str]:
586+
agent_access_key = self.agent_access_key
587+
if agent_access_key is None:
588+
return {}
589+
return {"Authorization": f"Bearer {agent_access_key}"}
590+
545591
@property
546592
@override
547593
def default_headers(self) -> dict[str, str | Omit]:
@@ -558,6 +604,16 @@ def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None:
558604
if isinstance(custom_headers.get("Authorization"), Omit):
559605
return
560606

607+
if self.model_access_key and headers.get("Authorization"):
608+
return
609+
if isinstance(custom_headers.get("Authorization"), Omit):
610+
return
611+
612+
if self.agent_access_key and headers.get("Authorization"):
613+
return
614+
if isinstance(custom_headers.get("Authorization"), Omit):
615+
return
616+
561617
raise TypeError(
562618
'"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"'
563619
)

src/gradient/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
22

33
__title__ = "gradient"
4-
__version__ = "3.4.0" # x-release-please-version
4+
__version__ = "3.5.0" # x-release-please-version

tests/test_client.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -414,12 +414,14 @@ def test_validate_headers(self) -> None:
414414
request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
415415
assert request.headers.get("Authorization") == f"Bearer {access_token}"
416416

417-
with update_env(**{"DIGITALOCEAN_ACCESS_TOKEN": Omit()}):
417+
with update_env(
418+
**{"DIGITALOCEAN_ACCESS_TOKEN": Omit(), "MODEL_ACCESS_KEY": Omit(), "AGENT_ACCESS_KEY": Omit()}
419+
):
418420
client2 = Gradient(
419421
base_url=base_url,
420422
access_token=None,
421-
model_access_key=model_access_key,
422-
agent_access_key=agent_access_key,
423+
model_access_key=None,
424+
agent_access_key=None,
423425
_strict_response_validation=True,
424426
)
425427

@@ -1429,12 +1431,14 @@ def test_validate_headers(self) -> None:
14291431
request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
14301432
assert request.headers.get("Authorization") == f"Bearer {access_token}"
14311433

1432-
with update_env(**{"DIGITALOCEAN_ACCESS_TOKEN": Omit()}):
1434+
with update_env(
1435+
**{"DIGITALOCEAN_ACCESS_TOKEN": Omit(), "MODEL_ACCESS_KEY": Omit(), "AGENT_ACCESS_KEY": Omit()}
1436+
):
14331437
client2 = AsyncGradient(
14341438
base_url=base_url,
14351439
access_token=None,
1436-
model_access_key=model_access_key,
1437-
agent_access_key=agent_access_key,
1440+
model_access_key=None,
1441+
agent_access_key=None,
14381442
_strict_response_validation=True,
14391443
)
14401444

0 commit comments

Comments
 (0)