From 1f04b9ecca3a4a3d2235c5cfa21bd9b36a358754 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 15 May 2025 04:19:14 +0000
Subject: [PATCH 01/35] chore(ci): upload sdks to package manager
---
.github/workflows/ci.yml | 24 ++++++++++++++++++++++++
scripts/utils/upload-artifact.sh | 25 +++++++++++++++++++++++++
2 files changed, 49 insertions(+)
create mode 100755 scripts/utils/upload-artifact.sh
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0ea316c..45646ff 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,6 +30,30 @@ jobs:
- name: Run lints
run: ./scripts/lint
+ upload:
+ if: github.repository == 'stainless-sdks/sunrise-python'
+ timeout-minutes: 10
+ name: upload
+ permissions:
+ contents: read
+ id-token: write
+ runs-on: depot-ubuntu-24.04
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Get GitHub OIDC Token
+ id: github-oidc
+ uses: actions/github-script@v6
+ with:
+ script: core.setOutput('github_token', await core.getIDToken());
+
+ - name: Upload tarball
+ env:
+ URL: https://pkg.stainless.com/s
+ AUTH: ${{ steps.github-oidc.outputs.github_token }}
+ SHA: ${{ github.sha }}
+ run: ./scripts/utils/upload-artifact.sh
+
test:
timeout-minutes: 10
name: test
diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh
new file mode 100755
index 0000000..d87f5fb
--- /dev/null
+++ b/scripts/utils/upload-artifact.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+set -exuo pipefail
+
+RESPONSE=$(curl -X POST "$URL" \
+ -H "Authorization: Bearer $AUTH" \
+ -H "Content-Type: application/json")
+
+SIGNED_URL=$(echo "$RESPONSE" | jq -r '.url')
+
+if [[ "$SIGNED_URL" == "null" ]]; then
+ echo -e "\033[31mFailed to get signed URL.\033[0m"
+ exit 1
+fi
+
+UPLOAD_RESPONSE=$(tar -cz . | curl -v -X PUT \
+ -H "Content-Type: application/gzip" \
+ --data-binary @- "$SIGNED_URL" 2>&1)
+
+if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then
+ echo -e "\033[32mUploaded build to Stainless storage.\033[0m"
+ echo -e "\033[32mInstallation: npm install 'https://pkg.stainless.com/s/sunrise-python/$SHA'\033[0m"
+else
+ echo -e "\033[31mFailed to upload artifact.\033[0m"
+ exit 1
+fi
From f191464e75f48395e76d6007712ae8548268b45f Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 16 May 2025 03:26:21 +0000
Subject: [PATCH 02/35] chore(ci): fix installation instructions
---
scripts/utils/upload-artifact.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh
index d87f5fb..8ce93e3 100755
--- a/scripts/utils/upload-artifact.sh
+++ b/scripts/utils/upload-artifact.sh
@@ -18,7 +18,7 @@ UPLOAD_RESPONSE=$(tar -cz . | curl -v -X PUT \
if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then
echo -e "\033[32mUploaded build to Stainless storage.\033[0m"
- echo -e "\033[32mInstallation: npm install 'https://pkg.stainless.com/s/sunrise-python/$SHA'\033[0m"
+ echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/sunrise-python/$SHA'\033[0m"
else
echo -e "\033[31mFailed to upload artifact.\033[0m"
exit 1
From 01370fb62278f1def879352910c2520102c89993 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 22 May 2025 02:34:34 +0000
Subject: [PATCH 03/35] chore(docs): grammar improvements
---
SECURITY.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/SECURITY.md b/SECURITY.md
index 92a473f..97e18f0 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -16,11 +16,11 @@ before making any information public.
## Reporting Non-SDK Related Security Issues
If you encounter security issues that are not directly related to SDKs but pertain to the services
-or products provided by Contextual AI please follow the respective company's security reporting guidelines.
+or products provided by Contextual AI, please follow the respective company's security reporting guidelines.
### Contextual AI Terms and Policies
-Please contact support@contextual.ai for any questions or concerns regarding security of our services.
+Please contact support@contextual.ai for any questions or concerns regarding the security of our services.
---
From 9fd7133c6748ba1b1676a674da35d57f02f01a86 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 28 May 2025 03:31:23 +0000
Subject: [PATCH 04/35] fix(docs/api): remove references to nonexistent types
---
api.md | 67 +++++++++++++++-------------------------------------------
1 file changed, 17 insertions(+), 50 deletions(-)
diff --git a/api.md b/api.md
index 34b9ced..900abd8 100644
--- a/api.md
+++ b/api.md
@@ -8,8 +8,6 @@ from contextual.types import (
Datastore,
DatastoreMetadata,
ListDatastoresResponse,
- DatastoreDeleteResponse,
- DatastoreResetResponse,
)
```
@@ -17,9 +15,9 @@ Methods:
- client.datastores.create(\*\*params) -> CreateDatastoreResponse
- client.datastores.list(\*\*params) -> SyncDatastoresPage[Datastore]
-- client.datastores.delete(datastore_id) -> object
+- client.datastores.delete(datastore_id) -> object
- client.datastores.metadata(datastore_id) -> DatastoreMetadata
-- client.datastores.reset(datastore_id) -> object
+- client.datastores.reset(datastore_id) -> object
## Documents
@@ -31,14 +29,13 @@ from contextual.types.datastores import (
DocumentMetadata,
IngestionResponse,
ListDocumentsResponse,
- DocumentDeleteResponse,
)
```
Methods:
- client.datastores.documents.list(datastore_id, \*\*params) -> SyncDocumentsPage[DocumentMetadata]
-- client.datastores.documents.delete(document_id, \*, datastore_id) -> object
+- client.datastores.documents.delete(document_id, \*, datastore_id) -> object
- client.datastores.documents.ingest(datastore_id, \*\*params) -> IngestionResponse
- client.datastores.documents.metadata(document_id, \*, datastore_id) -> DocumentMetadata
- client.datastores.documents.set_metadata(document_id, \*, datastore_id, \*\*params) -> DocumentMetadata
@@ -58,39 +55,31 @@ from contextual.types import (
GlobalConfig,
ListAgentsResponse,
RetrievalConfig,
- AgentUpdateResponse,
- AgentDeleteResponse,
AgentMetadataResponse,
- AgentResetResponse,
)
```
Methods:
- client.agents.create(\*\*params) -> CreateAgentOutput
-- client.agents.update(agent_id, \*\*params) -> object
+- client.agents.update(agent_id, \*\*params) -> object
- client.agents.list(\*\*params) -> SyncPage[Agent]
-- client.agents.delete(agent_id) -> object
+- client.agents.delete(agent_id) -> object
- client.agents.metadata(agent_id) -> AgentMetadataResponse
-- client.agents.reset(agent_id) -> object
+- client.agents.reset(agent_id) -> object
## Query
Types:
```python
-from contextual.types.agents import (
- QueryResponse,
- RetrievalInfoResponse,
- QueryFeedbackResponse,
- QueryMetricsResponse,
-)
+from contextual.types.agents import QueryResponse, RetrievalInfoResponse, QueryMetricsResponse
```
Methods:
- client.agents.query.create(agent_id, \*\*params) -> QueryResponse
-- client.agents.query.feedback(agent_id, \*\*params) -> object
+- client.agents.query.feedback(agent_id, \*\*params) -> object
- client.agents.query.metrics(agent_id, \*\*params) -> QueryMetricsResponse
- client.agents.query.retrieval_info(message_id, \*, agent_id, \*\*params) -> RetrievalInfoResponse
@@ -111,17 +100,13 @@ Methods:
Types:
```python
-from contextual.types.agents.evaluate import (
- EvaluationJobMetadata,
- ListEvaluationJobsResponse,
- JobCancelResponse,
-)
+from contextual.types.agents.evaluate import EvaluationJobMetadata, ListEvaluationJobsResponse
```
Methods:
- client.agents.evaluate.jobs.list(agent_id) -> ListEvaluationJobsResponse
-- client.agents.evaluate.jobs.cancel(job_id, \*, agent_id) -> object
+- client.agents.evaluate.jobs.cancel(job_id, \*, agent_id) -> object
- client.agents.evaluate.jobs.metadata(job_id, \*, agent_id) -> EvaluationJobMetadata
## Datasets
@@ -134,36 +119,24 @@ from contextual.types.agents import CreateDatasetResponse, DatasetMetadata, List
### Tune
-Types:
-
-```python
-from contextual.types.agents.datasets import TuneDeleteResponse
-```
-
Methods:
- client.agents.datasets.tune.create(agent_id, \*\*params) -> CreateDatasetResponse
- client.agents.datasets.tune.retrieve(dataset_name, \*, agent_id, \*\*params) -> BinaryAPIResponse
- client.agents.datasets.tune.update(dataset_name, \*, agent_id, \*\*params) -> CreateDatasetResponse
- client.agents.datasets.tune.list(agent_id, \*\*params) -> ListDatasetsResponse
-- client.agents.datasets.tune.delete(dataset_name, \*, agent_id) -> object
+- client.agents.datasets.tune.delete(dataset_name, \*, agent_id) -> object
- client.agents.datasets.tune.metadata(dataset_name, \*, agent_id, \*\*params) -> DatasetMetadata
### Evaluate
-Types:
-
-```python
-from contextual.types.agents.datasets import EvaluateDeleteResponse
-```
-
Methods:
- client.agents.datasets.evaluate.create(agent_id, \*\*params) -> CreateDatasetResponse
- client.agents.datasets.evaluate.retrieve(dataset_name, \*, agent_id, \*\*params) -> BinaryAPIResponse
- client.agents.datasets.evaluate.update(dataset_name, \*, agent_id, \*\*params) -> CreateDatasetResponse
- client.agents.datasets.evaluate.list(agent_id, \*\*params) -> ListDatasetsResponse
-- client.agents.datasets.evaluate.delete(dataset_name, \*, agent_id) -> object
+- client.agents.datasets.evaluate.delete(dataset_name, \*, agent_id) -> object
- client.agents.datasets.evaluate.metadata(dataset_name, \*, agent_id, \*\*params) -> DatasetMetadata
## Tune
@@ -183,13 +156,13 @@ Methods:
Types:
```python
-from contextual.types.agents.tune import ListTuneJobsResponse, TuneJobMetadata, JobDeleteResponse
+from contextual.types.agents.tune import ListTuneJobsResponse, TuneJobMetadata
```
Methods:
- client.agents.tune.jobs.list(agent_id) -> ListTuneJobsResponse
-- client.agents.tune.jobs.delete(job_id, \*, agent_id) -> object
+- client.agents.tune.jobs.delete(job_id, \*, agent_id) -> object
- client.agents.tune.jobs.metadata(job_id, \*, agent_id) -> TuneJobMetadata
### Models
@@ -209,20 +182,14 @@ Methods:
Types:
```python
-from contextual.types import (
- InviteUsersResponse,
- ListUsersResponse,
- NewUser,
- UserUpdateResponse,
- UserDeactivateResponse,
-)
+from contextual.types import InviteUsersResponse, ListUsersResponse, NewUser
```
Methods:
-- client.users.update(\*\*params) -> object
+- client.users.update(\*\*params) -> object
- client.users.list(\*\*params) -> SyncUsersPage[User]
-- client.users.deactivate(\*\*params) -> object
+- client.users.deactivate(\*\*params) -> object
- client.users.invite(\*\*params) -> InviteUsersResponse
# LMUnit
From 68f70a88e5b45773140c4b4a02c0506f3d078ad9 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 3 Jun 2025 02:26:42 +0000
Subject: [PATCH 05/35] chore(docs): remove reference to rye shell
---
CONTRIBUTING.md | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ab82b5a..3c21def 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -17,8 +17,7 @@ $ rye sync --all-features
You can then run scripts using `rye run python script.py` or by activating the virtual environment:
```sh
-$ rye shell
-# or manually activate - https://docs.python.org/3/library/venv.html#how-venvs-work
+# Activate the virtual environment - https://docs.python.org/3/library/venv.html#how-venvs-work
$ source .venv/bin/activate
# now you can omit the `rye run` prefix
From f603dcdd966c77ce3e8b8dba8e878eb273ef1688 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 3 Jun 2025 02:38:57 +0000
Subject: [PATCH 06/35] chore(docs): remove unnecessary param examples
---
README.md | 28 +---------------------------
1 file changed, 1 insertion(+), 27 deletions(-)
diff --git a/README.md b/README.md
index 4be5749..8863beb 100644
--- a/README.md
+++ b/README.md
@@ -149,33 +149,7 @@ client = ContextualAI()
create_agent_output = client.agents.create(
name="xxx",
- agent_configs={
- "filter_and_rerank_config": {
- "rerank_instructions": "rerank_instructions",
- "reranker_score_filter_threshold": 0,
- "top_k_reranked_chunks": 0,
- },
- "generate_response_config": {
- "avoid_commentary": True,
- "calculate_groundedness": True,
- "frequency_penalty": 0,
- "max_new_tokens": 0,
- "seed": 0,
- "temperature": 0,
- "top_p": 0,
- },
- "global_config": {
- "enable_filter": True,
- "enable_multi_turn": True,
- "enable_rerank": True,
- "should_check_retrieval_need": True,
- },
- "retrieval_config": {
- "lexical_alpha": 0,
- "semantic_alpha": 0,
- "top_k_retrieved_chunks": 0,
- },
- },
+ agent_configs={},
)
print(create_agent_output.agent_configs)
```
From 35e7c78c7d1801a0afe4d73bbff3e7c695f5f19f Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 3 Jun 2025 03:42:45 +0000
Subject: [PATCH 07/35] feat(client): add follow_redirects request option
---
src/contextual/_base_client.py | 6 ++++
src/contextual/_models.py | 2 ++
src/contextual/_types.py | 2 ++
tests/test_client.py | 54 ++++++++++++++++++++++++++++++++++
4 files changed, 64 insertions(+)
diff --git a/src/contextual/_base_client.py b/src/contextual/_base_client.py
index 7369dca..7f2624f 100644
--- a/src/contextual/_base_client.py
+++ b/src/contextual/_base_client.py
@@ -960,6 +960,9 @@ def request(
if self.custom_auth is not None:
kwargs["auth"] = self.custom_auth
+ if options.follow_redirects is not None:
+ kwargs["follow_redirects"] = options.follow_redirects
+
log.debug("Sending HTTP Request: %s %s", request.method, request.url)
response = None
@@ -1460,6 +1463,9 @@ async def request(
if self.custom_auth is not None:
kwargs["auth"] = self.custom_auth
+ if options.follow_redirects is not None:
+ kwargs["follow_redirects"] = options.follow_redirects
+
log.debug("Sending HTTP Request: %s %s", request.method, request.url)
response = None
diff --git a/src/contextual/_models.py b/src/contextual/_models.py
index 798956f..4f21498 100644
--- a/src/contextual/_models.py
+++ b/src/contextual/_models.py
@@ -737,6 +737,7 @@ class FinalRequestOptionsInput(TypedDict, total=False):
idempotency_key: str
json_data: Body
extra_json: AnyMapping
+ follow_redirects: bool
@final
@@ -750,6 +751,7 @@ class FinalRequestOptions(pydantic.BaseModel):
files: Union[HttpxRequestFiles, None] = None
idempotency_key: Union[str, None] = None
post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven()
+ follow_redirects: Union[bool, None] = None
# It should be noted that we cannot use `json` here as that would override
# a BaseModel method in an incompatible fashion.
diff --git a/src/contextual/_types.py b/src/contextual/_types.py
index 883a2da..46a038e 100644
--- a/src/contextual/_types.py
+++ b/src/contextual/_types.py
@@ -100,6 +100,7 @@ class RequestOptions(TypedDict, total=False):
params: Query
extra_json: AnyMapping
idempotency_key: str
+ follow_redirects: bool
# Sentinel class used until PEP 0661 is accepted
@@ -215,3 +216,4 @@ class _GenericAlias(Protocol):
class HttpxSendArgs(TypedDict, total=False):
auth: httpx.Auth
+ follow_redirects: bool
diff --git a/tests/test_client.py b/tests/test_client.py
index 4617f67..a8bfe5c 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -828,6 +828,33 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
assert response.http_request.headers.get("x-stainless-retry-count") == "42"
+ @pytest.mark.respx(base_url=base_url)
+ def test_follow_redirects(self, respx_mock: MockRouter) -> None:
+ # Test that the default follow_redirects=True allows following redirects
+ respx_mock.post("/redirect").mock(
+ return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
+ )
+ respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
+
+ response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
+ assert response.status_code == 200
+ assert response.json() == {"status": "ok"}
+
+ @pytest.mark.respx(base_url=base_url)
+ def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
+ # Test that follow_redirects=False prevents following redirects
+ respx_mock.post("/redirect").mock(
+ return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
+ )
+
+ with pytest.raises(APIStatusError) as exc_info:
+ self.client.post(
+ "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
+ )
+
+ assert exc_info.value.response.status_code == 302
+ assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"
+
class TestAsyncContextualAI:
client = AsyncContextualAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
@@ -1657,3 +1684,30 @@ async def test_main() -> None:
raise AssertionError("calling get_platform using asyncify resulted in a hung process")
time.sleep(0.1)
+
+ @pytest.mark.respx(base_url=base_url)
+ async def test_follow_redirects(self, respx_mock: MockRouter) -> None:
+ # Test that the default follow_redirects=True allows following redirects
+ respx_mock.post("/redirect").mock(
+ return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
+ )
+ respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
+
+ response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
+ assert response.status_code == 200
+ assert response.json() == {"status": "ok"}
+
+ @pytest.mark.respx(base_url=base_url)
+ async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
+ # Test that follow_redirects=False prevents following redirects
+ respx_mock.post("/redirect").mock(
+ return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
+ )
+
+ with pytest.raises(APIStatusError) as exc_info:
+ await self.client.post(
+ "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
+ )
+
+ assert exc_info.value.response.status_code == 302
+ assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"
From f75c912ff643028317dde5fb0dfd08470b26ac29 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 13 Jun 2025 02:12:55 +0000
Subject: [PATCH 08/35] chore(tests): run tests in parallel
---
pyproject.toml | 3 ++-
requirements-dev.lock | 4 ++++
2 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index 9a3b763..8d72211 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -54,6 +54,7 @@ dev-dependencies = [
"importlib-metadata>=6.7.0",
"rich>=13.7.1",
"nest_asyncio==1.6.0",
+ "pytest-xdist>=3.6.1",
]
[tool.rye.scripts]
@@ -125,7 +126,7 @@ replacement = '[\1](https://github.com/ContextualAI/contextual-client-python/tre
[tool.pytest.ini_options]
testpaths = ["tests"]
-addopts = "--tb=short"
+addopts = "--tb=short -n auto"
xfail_strict = true
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "session"
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 42c2907..7680c2b 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -30,6 +30,8 @@ distro==1.8.0
exceptiongroup==1.2.2
# via anyio
# via pytest
+execnet==2.1.1
+ # via pytest-xdist
filelock==3.12.4
# via virtualenv
h11==0.14.0
@@ -72,7 +74,9 @@ pygments==2.18.0
pyright==1.1.399
pytest==8.3.3
# via pytest-asyncio
+ # via pytest-xdist
pytest-asyncio==0.24.0
+pytest-xdist==3.7.0
python-dateutil==2.8.2
# via time-machine
pytz==2023.3.post1
From 518cbabda3ce7f53721c0fc916ae89706899a4ec Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 13 Jun 2025 02:37:22 +0000
Subject: [PATCH 09/35] fix(client): correctly parse binary response | stream
---
src/contextual/_base_client.py | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/src/contextual/_base_client.py b/src/contextual/_base_client.py
index 7f2624f..ec3767e 100644
--- a/src/contextual/_base_client.py
+++ b/src/contextual/_base_client.py
@@ -1071,7 +1071,14 @@ def _process_response(
) -> ResponseT:
origin = get_origin(cast_to) or cast_to
- if inspect.isclass(origin) and issubclass(origin, BaseAPIResponse):
+ if (
+ inspect.isclass(origin)
+ and issubclass(origin, BaseAPIResponse)
+ # we only want to actually return the custom BaseAPIResponse class if we're
+ # returning the raw response, or if we're not streaming SSE, as if we're streaming
+ # SSE then `cast_to` doesn't actively reflect the type we need to parse into
+ and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER)))
+ ):
if not issubclass(origin, APIResponse):
raise TypeError(f"API Response types must subclass {APIResponse}; Received {origin}")
@@ -1574,7 +1581,14 @@ async def _process_response(
) -> ResponseT:
origin = get_origin(cast_to) or cast_to
- if inspect.isclass(origin) and issubclass(origin, BaseAPIResponse):
+ if (
+ inspect.isclass(origin)
+ and issubclass(origin, BaseAPIResponse)
+ # we only want to actually return the custom BaseAPIResponse class if we're
+ # returning the raw response, or if we're not streaming SSE, as if we're streaming
+ # SSE then `cast_to` doesn't actively reflect the type we need to parse into
+ and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER)))
+ ):
if not issubclass(origin, AsyncAPIResponse):
raise TypeError(f"API Response types must subclass {AsyncAPIResponse}; Received {origin}")
From 0c4973fed123a77a16b189439b3f4976fcc91770 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 17 Jun 2025 02:42:29 +0000
Subject: [PATCH 10/35] chore(tests): add tests for httpx client instantiation
& proxies
---
tests/test_client.py | 46 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 46 insertions(+)
diff --git a/tests/test_client.py b/tests/test_client.py
index a8bfe5c..bf5b6ce 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -31,6 +31,8 @@
DEFAULT_TIMEOUT,
HTTPX_DEFAULT_TIMEOUT,
BaseClient,
+ DefaultHttpxClient,
+ DefaultAsyncHttpxClient,
make_request_options,
)
from contextual.types.agent_create_params import AgentCreateParams
@@ -828,6 +830,28 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
assert response.http_request.headers.get("x-stainless-retry-count") == "42"
+ def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ # Test that the proxy environment variables are set correctly
+ monkeypatch.setenv("HTTPS_PROXY", "https://example.org")
+
+ client = DefaultHttpxClient()
+
+ mounts = tuple(client._mounts.items())
+ assert len(mounts) == 1
+ assert mounts[0][0].pattern == "https://"
+
+ @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning")
+ def test_default_client_creation(self) -> None:
+ # Ensure that the client can be initialized without any exceptions
+ DefaultHttpxClient(
+ verify=True,
+ cert=None,
+ trust_env=True,
+ http1=True,
+ http2=False,
+ limits=httpx.Limits(max_connections=100, max_keepalive_connections=20),
+ )
+
@pytest.mark.respx(base_url=base_url)
def test_follow_redirects(self, respx_mock: MockRouter) -> None:
# Test that the default follow_redirects=True allows following redirects
@@ -1685,6 +1709,28 @@ async def test_main() -> None:
time.sleep(0.1)
+ async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ # Test that the proxy environment variables are set correctly
+ monkeypatch.setenv("HTTPS_PROXY", "https://example.org")
+
+ client = DefaultAsyncHttpxClient()
+
+ mounts = tuple(client._mounts.items())
+ assert len(mounts) == 1
+ assert mounts[0][0].pattern == "https://"
+
+ @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning")
+ async def test_default_client_creation(self) -> None:
+ # Ensure that the client can be initialized without any exceptions
+ DefaultAsyncHttpxClient(
+ verify=True,
+ cert=None,
+ trust_env=True,
+ http1=True,
+ http2=False,
+ limits=httpx.Limits(max_connections=100, max_keepalive_connections=20),
+ )
+
@pytest.mark.respx(base_url=base_url)
async def test_follow_redirects(self, respx_mock: MockRouter) -> None:
# Test that the default follow_redirects=True allows following redirects
From b324ed373c9c174a44eb52dc6d2384e82c0af4b8 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 17 Jun 2025 04:12:38 +0000
Subject: [PATCH 11/35] chore(internal): update conftest.py
---
tests/conftest.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/tests/conftest.py b/tests/conftest.py
index aa99261..5f3d8ba 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,3 +1,5 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
from __future__ import annotations
import os
From 84fbba4c22dbbf8517841c7961a37dba246126dc Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 17 Jun 2025 06:43:28 +0000
Subject: [PATCH 12/35] chore(ci): enable for pull requests
---
.github/workflows/ci.yml | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 45646ff..381a918 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,6 +7,10 @@ on:
- 'integrated/**'
- 'stl-preview-head/**'
- 'stl-preview-base/**'
+ pull_request:
+ branches-ignore:
+ - 'stl-preview-head/**'
+ - 'stl-preview-base/**'
jobs:
lint:
From b747f452ab31df0805dd07a516fe63c460353c57 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 18 Jun 2025 02:15:34 +0000
Subject: [PATCH 13/35] chore(readme): update badges
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 8863beb..4659d84 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Contextual AI Python API library
-[](https://pypi.org/project/contextual-client/)
+[![PyPI version]()](https://pypi.org/project/contextual-client/)
The Contextual AI Python library provides convenient access to the Contextual AI REST API from any Python 3.8+
application. The library includes type definitions for all request params and response fields,
From d7920f111d6175e6714482918e34992fb51739d9 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 18 Jun 2025 05:52:30 +0000
Subject: [PATCH 14/35] fix(tests): fix: tests which call HTTP endpoints
directly with the example parameters
---
tests/test_client.py | 45 ++++++++++++--------------------------------
1 file changed, 12 insertions(+), 33 deletions(-)
diff --git a/tests/test_client.py b/tests/test_client.py
index bf5b6ce..12b2537 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -23,9 +23,7 @@
from contextual import ContextualAI, AsyncContextualAI, APIResponseValidationError
from contextual._types import Omit
-from contextual._utils import maybe_transform
from contextual._models import BaseModel, FinalRequestOptions
-from contextual._constants import RAW_RESPONSE_HEADER
from contextual._exceptions import APIStatusError, APITimeoutError, ContextualAIError, APIResponseValidationError
from contextual._base_client import (
DEFAULT_TIMEOUT,
@@ -35,7 +33,6 @@
DefaultAsyncHttpxClient,
make_request_options,
)
-from contextual.types.agent_create_params import AgentCreateParams
from .utils import update_env
@@ -725,32 +722,21 @@ def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str
@mock.patch("contextual._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
- def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None:
+ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: ContextualAI) -> None:
respx_mock.post("/agents").mock(side_effect=httpx.TimeoutException("Test timeout error"))
with pytest.raises(APITimeoutError):
- self.client.post(
- "/agents",
- body=cast(object, maybe_transform(dict(name="Example"), AgentCreateParams)),
- cast_to=httpx.Response,
- options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
- )
+ client.agents.with_streaming_response.create(name="xxx").__enter__()
assert _get_open_connections(self.client) == 0
@mock.patch("contextual._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
- def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None:
+ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: ContextualAI) -> None:
respx_mock.post("/agents").mock(return_value=httpx.Response(500))
with pytest.raises(APIStatusError):
- self.client.post(
- "/agents",
- body=cast(object, maybe_transform(dict(name="Example"), AgentCreateParams)),
- cast_to=httpx.Response,
- options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
- )
-
+ client.agents.with_streaming_response.create(name="xxx").__enter__()
assert _get_open_connections(self.client) == 0
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
@@ -1550,32 +1536,25 @@ async def test_parse_retry_after_header(self, remaining_retries: int, retry_afte
@mock.patch("contextual._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
- async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None:
+ async def test_retrying_timeout_errors_doesnt_leak(
+ self, respx_mock: MockRouter, async_client: AsyncContextualAI
+ ) -> None:
respx_mock.post("/agents").mock(side_effect=httpx.TimeoutException("Test timeout error"))
with pytest.raises(APITimeoutError):
- await self.client.post(
- "/agents",
- body=cast(object, maybe_transform(dict(name="Example"), AgentCreateParams)),
- cast_to=httpx.Response,
- options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
- )
+ await async_client.agents.with_streaming_response.create(name="xxx").__aenter__()
assert _get_open_connections(self.client) == 0
@mock.patch("contextual._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
- async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None:
+ async def test_retrying_status_errors_doesnt_leak(
+ self, respx_mock: MockRouter, async_client: AsyncContextualAI
+ ) -> None:
respx_mock.post("/agents").mock(return_value=httpx.Response(500))
with pytest.raises(APIStatusError):
- await self.client.post(
- "/agents",
- body=cast(object, maybe_transform(dict(name="Example"), AgentCreateParams)),
- cast_to=httpx.Response,
- options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
- )
-
+ await async_client.agents.with_streaming_response.create(name="xxx").__aenter__()
assert _get_open_connections(self.client) == 0
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
From 3517a3d02c7447c027bc82baf3a83333eb3c9b55 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 19 Jun 2025 02:53:32 +0000
Subject: [PATCH 15/35] docs(client): fix httpx.Timeout documentation reference
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 4659d84..e4997ac 100644
--- a/README.md
+++ b/README.md
@@ -241,7 +241,7 @@ client.with_options(max_retries=5).agents.create(
### Timeouts
By default requests time out after 1 minute. You can configure this with a `timeout` option,
-which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/#fine-tuning-the-configuration) object:
+which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object:
```python
from contextual import ContextualAI
From d54f53cfa0878acbad344622f7aae1b2e939ae1c Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 21 Jun 2025 04:12:21 +0000
Subject: [PATCH 16/35] feat(client): add support for aiohttp
---
README.md | 34 +++++++++++++++
pyproject.toml | 2 +
requirements-dev.lock | 27 ++++++++++++
requirements.lock | 27 ++++++++++++
src/contextual/__init__.py | 3 +-
src/contextual/_base_client.py | 22 ++++++++++
.../agents/datasets/test_evaluate.py | 4 +-
.../agents/datasets/test_tune.py | 4 +-
.../agents/evaluate/test_jobs.py | 4 +-
tests/api_resources/agents/test_evaluate.py | 4 +-
tests/api_resources/agents/test_query.py | 4 +-
tests/api_resources/agents/test_tune.py | 4 +-
tests/api_resources/agents/tune/test_jobs.py | 4 +-
.../api_resources/agents/tune/test_models.py | 4 +-
.../datastores/test_documents.py | 4 +-
tests/api_resources/test_agents.py | 4 +-
tests/api_resources/test_datastores.py | 4 +-
tests/api_resources/test_generate.py | 4 +-
tests/api_resources/test_lmunit.py | 4 +-
tests/api_resources/test_parse.py | 4 +-
tests/api_resources/test_rerank.py | 4 +-
tests/api_resources/test_users.py | 4 +-
tests/conftest.py | 43 ++++++++++++++++---
23 files changed, 199 insertions(+), 23 deletions(-)
diff --git a/README.md b/README.md
index e4997ac..19b3884 100644
--- a/README.md
+++ b/README.md
@@ -66,6 +66,40 @@ asyncio.run(main())
Functionality between the synchronous and asynchronous clients is otherwise identical.
+### With aiohttp
+
+By default, the async client uses `httpx` for HTTP requests. However, for improved concurrency performance you may also use `aiohttp` as the HTTP backend.
+
+You can enable this by installing `aiohttp`:
+
+```sh
+# install from PyPI
+pip install contextual-client[aiohttp]
+```
+
+Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`:
+
+```python
+import os
+import asyncio
+from contextual import DefaultAioHttpClient
+from contextual import AsyncContextualAI
+
+
+async def main() -> None:
+ async with AsyncContextualAI(
+ api_key=os.environ.get("CONTEXTUAL_API_KEY"), # This is the default and can be omitted
+ http_client=DefaultAioHttpClient(),
+ ) as client:
+ create_agent_output = await client.agents.create(
+ name="Example",
+ )
+ print(create_agent_output.id)
+
+
+asyncio.run(main())
+```
+
## Using types
Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like:
diff --git a/pyproject.toml b/pyproject.toml
index 8d72211..51667fd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -37,6 +37,8 @@ classifiers = [
Homepage = "https://github.com/ContextualAI/contextual-client-python"
Repository = "https://github.com/ContextualAI/contextual-client-python"
+[project.optional-dependencies]
+aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.6"]
[tool.rye]
managed = true
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 7680c2b..7ccc541 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -10,6 +10,13 @@
# universal: false
-e file:.
+aiohappyeyeballs==2.6.1
+ # via aiohttp
+aiohttp==3.12.8
+ # via contextual-client
+ # via httpx-aiohttp
+aiosignal==1.3.2
+ # via aiohttp
annotated-types==0.6.0
# via pydantic
anyio==4.4.0
@@ -17,6 +24,10 @@ anyio==4.4.0
# via httpx
argcomplete==3.1.2
# via nox
+async-timeout==5.0.1
+ # via aiohttp
+attrs==25.3.0
+ # via aiohttp
certifi==2023.7.22
# via httpcore
# via httpx
@@ -34,16 +45,23 @@ execnet==2.1.1
# via pytest-xdist
filelock==3.12.4
# via virtualenv
+frozenlist==1.6.2
+ # via aiohttp
+ # via aiosignal
h11==0.14.0
# via httpcore
httpcore==1.0.2
# via httpx
httpx==0.28.1
# via contextual-client
+ # via httpx-aiohttp
# via respx
+httpx-aiohttp==0.1.6
+ # via contextual-client
idna==3.4
# via anyio
# via httpx
+ # via yarl
importlib-metadata==7.0.0
iniconfig==2.0.0
# via pytest
@@ -51,6 +69,9 @@ markdown-it-py==3.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
+multidict==6.4.4
+ # via aiohttp
+ # via yarl
mypy==1.14.1
mypy-extensions==1.0.0
# via mypy
@@ -65,6 +86,9 @@ platformdirs==3.11.0
# via virtualenv
pluggy==1.5.0
# via pytest
+propcache==0.3.1
+ # via aiohttp
+ # via yarl
pydantic==2.10.3
# via contextual-client
pydantic-core==2.27.1
@@ -98,11 +122,14 @@ tomli==2.0.2
typing-extensions==4.12.2
# via anyio
# via contextual-client
+ # via multidict
# via mypy
# via pydantic
# via pydantic-core
# via pyright
virtualenv==20.24.5
# via nox
+yarl==1.20.0
+ # via aiohttp
zipp==3.17.0
# via importlib-metadata
diff --git a/requirements.lock b/requirements.lock
index bc4698e..f69333d 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -10,11 +10,22 @@
# universal: false
-e file:.
+aiohappyeyeballs==2.6.1
+ # via aiohttp
+aiohttp==3.12.8
+ # via contextual-client
+ # via httpx-aiohttp
+aiosignal==1.3.2
+ # via aiohttp
annotated-types==0.6.0
# via pydantic
anyio==4.4.0
# via contextual-client
# via httpx
+async-timeout==5.0.1
+ # via aiohttp
+attrs==25.3.0
+ # via aiohttp
certifi==2023.7.22
# via httpcore
# via httpx
@@ -22,15 +33,28 @@ distro==1.8.0
# via contextual-client
exceptiongroup==1.2.2
# via anyio
+frozenlist==1.6.2
+ # via aiohttp
+ # via aiosignal
h11==0.14.0
# via httpcore
httpcore==1.0.2
# via httpx
httpx==0.28.1
# via contextual-client
+ # via httpx-aiohttp
+httpx-aiohttp==0.1.6
+ # via contextual-client
idna==3.4
# via anyio
# via httpx
+ # via yarl
+multidict==6.4.4
+ # via aiohttp
+ # via yarl
+propcache==0.3.1
+ # via aiohttp
+ # via yarl
pydantic==2.10.3
# via contextual-client
pydantic-core==2.27.1
@@ -41,5 +65,8 @@ sniffio==1.3.0
typing-extensions==4.12.2
# via anyio
# via contextual-client
+ # via multidict
# via pydantic
# via pydantic-core
+yarl==1.20.0
+ # via aiohttp
diff --git a/src/contextual/__init__.py b/src/contextual/__init__.py
index c9d0895..831570c 100644
--- a/src/contextual/__init__.py
+++ b/src/contextual/__init__.py
@@ -36,7 +36,7 @@
UnprocessableEntityError,
APIResponseValidationError,
)
-from ._base_client import DefaultHttpxClient, DefaultAsyncHttpxClient
+from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient
from ._utils._logs import setup_logging as _setup_logging
__all__ = [
@@ -78,6 +78,7 @@
"DEFAULT_CONNECTION_LIMITS",
"DefaultHttpxClient",
"DefaultAsyncHttpxClient",
+ "DefaultAioHttpClient",
]
if not _t.TYPE_CHECKING:
diff --git a/src/contextual/_base_client.py b/src/contextual/_base_client.py
index ec3767e..fb8a539 100644
--- a/src/contextual/_base_client.py
+++ b/src/contextual/_base_client.py
@@ -1289,6 +1289,24 @@ def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
+try:
+ import httpx_aiohttp
+except ImportError:
+
+ 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
+ def __init__(self, **kwargs: Any) -> None:
+ kwargs.setdefault("timeout", DEFAULT_TIMEOUT)
+ kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS)
+ kwargs.setdefault("follow_redirects", True)
+
+ super().__init__(**kwargs)
+
+
if TYPE_CHECKING:
DefaultAsyncHttpxClient = httpx.AsyncClient
"""An alias to `httpx.AsyncClient` that provides the same defaults that this SDK
@@ -1297,8 +1315,12 @@ def __init__(self, **kwargs: Any) -> None:
This is useful because overriding the `http_client` with your own instance of
`httpx.AsyncClient` will result in httpx's defaults being used, not ours.
"""
+
+ DefaultAioHttpClient = httpx.AsyncClient
+ """An alias to `httpx.AsyncClient` that changes the default HTTP transport to `aiohttp`."""
else:
DefaultAsyncHttpxClient = _DefaultAsyncHttpxClient
+ DefaultAioHttpClient = _DefaultAioHttpClient
class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient):
diff --git a/tests/api_resources/agents/datasets/test_evaluate.py b/tests/api_resources/agents/datasets/test_evaluate.py
index 752fcd6..9ef82cd 100644
--- a/tests/api_resources/agents/datasets/test_evaluate.py
+++ b/tests/api_resources/agents/datasets/test_evaluate.py
@@ -369,7 +369,9 @@ def test_path_params_metadata(self, client: ContextualAI) -> None:
class TestAsyncEvaluate:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@parametrize
async def test_method_create(self, async_client: AsyncContextualAI) -> None:
diff --git a/tests/api_resources/agents/datasets/test_tune.py b/tests/api_resources/agents/datasets/test_tune.py
index 704dac4..ef47686 100644
--- a/tests/api_resources/agents/datasets/test_tune.py
+++ b/tests/api_resources/agents/datasets/test_tune.py
@@ -369,7 +369,9 @@ def test_path_params_metadata(self, client: ContextualAI) -> None:
class TestAsyncTune:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@parametrize
async def test_method_create(self, async_client: AsyncContextualAI) -> None:
diff --git a/tests/api_resources/agents/evaluate/test_jobs.py b/tests/api_resources/agents/evaluate/test_jobs.py
index 75f186f..353f37e 100644
--- a/tests/api_resources/agents/evaluate/test_jobs.py
+++ b/tests/api_resources/agents/evaluate/test_jobs.py
@@ -153,7 +153,9 @@ def test_path_params_metadata(self, client: ContextualAI) -> None:
class TestAsyncJobs:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@parametrize
async def test_method_list(self, async_client: AsyncContextualAI) -> None:
diff --git a/tests/api_resources/agents/test_evaluate.py b/tests/api_resources/agents/test_evaluate.py
index ab28d72..ad00bfb 100644
--- a/tests/api_resources/agents/test_evaluate.py
+++ b/tests/api_resources/agents/test_evaluate.py
@@ -74,7 +74,9 @@ def test_path_params_create(self, client: ContextualAI) -> None:
class TestAsyncEvaluate:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@parametrize
async def test_method_create(self, async_client: AsyncContextualAI) -> None:
diff --git a/tests/api_resources/agents/test_query.py b/tests/api_resources/agents/test_query.py
index 26b5b1d..e67f949 100644
--- a/tests/api_resources/agents/test_query.py
+++ b/tests/api_resources/agents/test_query.py
@@ -279,7 +279,9 @@ def test_path_params_retrieval_info(self, client: ContextualAI) -> None:
class TestAsyncQuery:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@parametrize
async def test_method_create(self, async_client: AsyncContextualAI) -> None:
diff --git a/tests/api_resources/agents/test_tune.py b/tests/api_resources/agents/test_tune.py
index c9dacef..964bdc4 100644
--- a/tests/api_resources/agents/test_tune.py
+++ b/tests/api_resources/agents/test_tune.py
@@ -77,7 +77,9 @@ def test_path_params_create(self, client: ContextualAI) -> None:
class TestAsyncTune:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@parametrize
async def test_method_create(self, async_client: AsyncContextualAI) -> None:
diff --git a/tests/api_resources/agents/tune/test_jobs.py b/tests/api_resources/agents/tune/test_jobs.py
index 0074de1..14a8cae 100644
--- a/tests/api_resources/agents/tune/test_jobs.py
+++ b/tests/api_resources/agents/tune/test_jobs.py
@@ -153,7 +153,9 @@ def test_path_params_metadata(self, client: ContextualAI) -> None:
class TestAsyncJobs:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@parametrize
async def test_method_list(self, async_client: AsyncContextualAI) -> None:
diff --git a/tests/api_resources/agents/tune/test_models.py b/tests/api_resources/agents/tune/test_models.py
index f2918fe..4b7185a 100644
--- a/tests/api_resources/agents/tune/test_models.py
+++ b/tests/api_resources/agents/tune/test_models.py
@@ -57,7 +57,9 @@ def test_path_params_list(self, client: ContextualAI) -> None:
class TestAsyncModels:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@parametrize
async def test_method_list(self, async_client: AsyncContextualAI) -> None:
diff --git a/tests/api_resources/datastores/test_documents.py b/tests/api_resources/datastores/test_documents.py
index 525988a..570ebfc 100644
--- a/tests/api_resources/datastores/test_documents.py
+++ b/tests/api_resources/datastores/test_documents.py
@@ -278,7 +278,9 @@ def test_path_params_set_metadata(self, client: ContextualAI) -> None:
class TestAsyncDocuments:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@parametrize
async def test_method_list(self, async_client: AsyncContextualAI) -> None:
diff --git a/tests/api_resources/test_agents.py b/tests/api_resources/test_agents.py
index 9009cee..a3e19ba 100644
--- a/tests/api_resources/test_agents.py
+++ b/tests/api_resources/test_agents.py
@@ -320,7 +320,9 @@ def test_path_params_reset(self, client: ContextualAI) -> None:
class TestAsyncAgents:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@parametrize
async def test_method_create(self, async_client: AsyncContextualAI) -> None:
diff --git a/tests/api_resources/test_datastores.py b/tests/api_resources/test_datastores.py
index fe99724..767d1f8 100644
--- a/tests/api_resources/test_datastores.py
+++ b/tests/api_resources/test_datastores.py
@@ -203,7 +203,9 @@ def test_path_params_reset(self, client: ContextualAI) -> None:
class TestAsyncDatastores:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@parametrize
async def test_method_create(self, async_client: AsyncContextualAI) -> None:
diff --git a/tests/api_resources/test_generate.py b/tests/api_resources/test_generate.py
index 44938ae..bd288b7 100644
--- a/tests/api_resources/test_generate.py
+++ b/tests/api_resources/test_generate.py
@@ -90,7 +90,9 @@ def test_streaming_response_create(self, client: ContextualAI) -> None:
class TestAsyncGenerate:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@parametrize
async def test_method_create(self, async_client: AsyncContextualAI) -> None:
diff --git a/tests/api_resources/test_lmunit.py b/tests/api_resources/test_lmunit.py
index 4c5a5a2..367b4bd 100644
--- a/tests/api_resources/test_lmunit.py
+++ b/tests/api_resources/test_lmunit.py
@@ -56,7 +56,9 @@ def test_streaming_response_create(self, client: ContextualAI) -> None:
class TestAsyncLMUnit:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@parametrize
async def test_method_create(self, async_client: AsyncContextualAI) -> None:
diff --git a/tests/api_resources/test_parse.py b/tests/api_resources/test_parse.py
index 08898d9..78a8a18 100644
--- a/tests/api_resources/test_parse.py
+++ b/tests/api_resources/test_parse.py
@@ -185,7 +185,9 @@ def test_streaming_response_jobs(self, client: ContextualAI) -> None:
class TestAsyncParse:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@parametrize
async def test_method_create(self, async_client: AsyncContextualAI) -> None:
diff --git a/tests/api_resources/test_rerank.py b/tests/api_resources/test_rerank.py
index 6f97de0..68fbefc 100644
--- a/tests/api_resources/test_rerank.py
+++ b/tests/api_resources/test_rerank.py
@@ -68,7 +68,9 @@ def test_streaming_response_create(self, client: ContextualAI) -> None:
class TestAsyncRerank:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@parametrize
async def test_method_create(self, async_client: AsyncContextualAI) -> None:
diff --git a/tests/api_resources/test_users.py b/tests/api_resources/test_users.py
index acd99a9..78edb82 100644
--- a/tests/api_resources/test_users.py
+++ b/tests/api_resources/test_users.py
@@ -163,7 +163,9 @@ def test_streaming_response_invite(self, client: ContextualAI) -> None:
class TestAsyncUsers:
- parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
@parametrize
async def test_method_update(self, async_client: AsyncContextualAI) -> None:
diff --git a/tests/conftest.py b/tests/conftest.py
index 5f3d8ba..125175d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -6,10 +6,12 @@
import logging
from typing import TYPE_CHECKING, Iterator, AsyncIterator
+import httpx
import pytest
from pytest_asyncio import is_async_test
-from contextual import ContextualAI, AsyncContextualAI
+from contextual import ContextualAI, AsyncContextualAI, DefaultAioHttpClient
+from contextual._utils import is_dict
if TYPE_CHECKING:
from _pytest.fixtures import FixtureRequest # pyright: ignore[reportPrivateImportUsage]
@@ -27,6 +29,19 @@ def pytest_collection_modifyitems(items: list[pytest.Function]) -> None:
for async_test in pytest_asyncio_tests:
async_test.add_marker(session_scope_marker, append=False)
+ # We skip tests that use both the aiohttp client and respx_mock as respx_mock
+ # doesn't support custom transports.
+ for item in items:
+ if "async_client" not in item.fixturenames or "respx_mock" not in item.fixturenames:
+ continue
+
+ if not hasattr(item, "callspec"):
+ continue
+
+ async_client_param = item.callspec.params.get("async_client")
+ if is_dict(async_client_param) and async_client_param.get("http_client") == "aiohttp":
+ item.add_marker(pytest.mark.skip(reason="aiohttp client is not compatible with respx_mock"))
+
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -45,9 +60,25 @@ def client(request: FixtureRequest) -> Iterator[ContextualAI]:
@pytest.fixture(scope="session")
async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncContextualAI]:
- strict = getattr(request, "param", True)
- if not isinstance(strict, bool):
- raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}")
-
- async with AsyncContextualAI(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client:
+ param = getattr(request, "param", True)
+
+ # defaults
+ strict = True
+ http_client: None | httpx.AsyncClient = None
+
+ if isinstance(param, bool):
+ strict = param
+ elif is_dict(param):
+ strict = param.get("strict", True)
+ assert isinstance(strict, bool)
+
+ http_client_type = param.get("http_client", "httpx")
+ if http_client_type == "aiohttp":
+ http_client = DefaultAioHttpClient()
+ else:
+ raise TypeError(f"Unexpected fixture parameter type {type(param)}, expected bool or dict")
+
+ async with AsyncContextualAI(
+ base_url=base_url, api_key=api_key, _strict_response_validation=strict, http_client=http_client
+ ) as client:
yield client
From dd32830a8266dbf736c85285ec611854659511e7 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 24 Jun 2025 04:19:48 +0000
Subject: [PATCH 17/35] chore(tests): skip some failing tests on the latest
python versions
---
tests/test_client.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/tests/test_client.py b/tests/test_client.py
index 12b2537..15a6d55 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -191,6 +191,7 @@ def test_copy_signature(self) -> None:
copy_param = copy_signature.parameters.get(name)
assert copy_param is not None, f"copy() signature is missing the {name} param"
+ @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12")
def test_copy_build_request(self) -> None:
options = FinalRequestOptions(method="get", url="/foo")
@@ -1001,6 +1002,7 @@ def test_copy_signature(self) -> None:
copy_param = copy_signature.parameters.get(name)
assert copy_param is not None, f"copy() signature is missing the {name} param"
+ @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12")
def test_copy_build_request(self) -> None:
options = FinalRequestOptions(method="get", url="/foo")
From ce0af3be8b2f90af2bc4e38979a801df1e98e989 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 27 Jun 2025 02:37:06 +0000
Subject: [PATCH 18/35] =?UTF-8?q?fix(ci):=20release-doctor=20=E2=80=94=20r?=
=?UTF-8?q?eport=20correct=20token=20name?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
bin/check-release-environment | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/bin/check-release-environment b/bin/check-release-environment
index 4fafbfe..b845b0f 100644
--- a/bin/check-release-environment
+++ b/bin/check-release-environment
@@ -3,7 +3,7 @@
errors=()
if [ -z "${PYPI_TOKEN}" ]; then
- errors+=("The CONTEXTUAL_AI_PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.")
+ errors+=("The PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.")
fi
lenErrors=${#errors[@]}
From b9520a0ad9c16d3ad0386ce70a15df4191751364 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 28 Jun 2025 08:46:20 +0000
Subject: [PATCH 19/35] chore(ci): only run for pushes and fork pull requests
---
.github/workflows/ci.yml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 381a918..c97a085 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -17,6 +17,7 @@ jobs:
timeout-minutes: 10
name: lint
runs-on: ${{ github.repository == 'stainless-sdks/sunrise-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@v4
@@ -42,6 +43,7 @@ jobs:
contents: read
id-token: write
runs-on: depot-ubuntu-24.04
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@v4
@@ -62,6 +64,7 @@ jobs:
timeout-minutes: 10
name: test
runs-on: ${{ github.repository == 'stainless-sdks/sunrise-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@v4
From 0e1ab57132d5a038aac790b463166200ae436fc3 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 30 Jun 2025 02:32:03 +0000
Subject: [PATCH 20/35] fix(ci): correct conditional
---
.github/workflows/ci.yml | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c97a085..80da1b5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -36,14 +36,13 @@ jobs:
run: ./scripts/lint
upload:
- if: github.repository == 'stainless-sdks/sunrise-python'
+ if: github.repository == 'stainless-sdks/sunrise-python' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork)
timeout-minutes: 10
name: upload
permissions:
contents: read
id-token: write
runs-on: depot-ubuntu-24.04
- if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@v4
From f72dfb77ff1fcae80efa5b286800ed77af6d0889 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 2 Jul 2025 05:07:43 +0000
Subject: [PATCH 21/35] chore(ci): change upload type
---
.github/workflows/ci.yml | 18 ++++++++++++++++--
scripts/utils/upload-artifact.sh | 12 +++++++-----
2 files changed, 23 insertions(+), 7 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 80da1b5..242ae46 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -35,10 +35,10 @@ jobs:
- name: Run lints
run: ./scripts/lint
- upload:
+ build:
if: github.repository == 'stainless-sdks/sunrise-python' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork)
timeout-minutes: 10
- name: upload
+ name: build
permissions:
contents: read
id-token: write
@@ -46,6 +46,20 @@ jobs:
steps:
- uses: actions/checkout@v4
+ - name: Install Rye
+ run: |
+ curl -sSf https://rye.astral.sh/get | bash
+ echo "$HOME/.rye/shims" >> $GITHUB_PATH
+ env:
+ RYE_VERSION: '0.44.0'
+ RYE_INSTALL_OPTION: '--yes'
+
+ - name: Install dependencies
+ run: rye sync --all-features
+
+ - name: Run build
+ run: rye build
+
- name: Get GitHub OIDC Token
id: github-oidc
uses: actions/github-script@v6
diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh
index 8ce93e3..6fb2114 100755
--- a/scripts/utils/upload-artifact.sh
+++ b/scripts/utils/upload-artifact.sh
@@ -1,7 +1,9 @@
#!/usr/bin/env bash
set -exuo pipefail
-RESPONSE=$(curl -X POST "$URL" \
+FILENAME=$(basename dist/*.whl)
+
+RESPONSE=$(curl -X POST "$URL?filename=$FILENAME" \
-H "Authorization: Bearer $AUTH" \
-H "Content-Type: application/json")
@@ -12,13 +14,13 @@ if [[ "$SIGNED_URL" == "null" ]]; then
exit 1
fi
-UPLOAD_RESPONSE=$(tar -cz . | curl -v -X PUT \
- -H "Content-Type: application/gzip" \
- --data-binary @- "$SIGNED_URL" 2>&1)
+UPLOAD_RESPONSE=$(curl -v -X PUT \
+ -H "Content-Type: binary/octet-stream" \
+ --data-binary "@dist/$FILENAME" "$SIGNED_URL" 2>&1)
if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then
echo -e "\033[32mUploaded build to Stainless storage.\033[0m"
- echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/sunrise-python/$SHA'\033[0m"
+ echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/sunrise-python/$SHA/$FILENAME'\033[0m"
else
echo -e "\033[31mFailed to upload artifact.\033[0m"
exit 1
From 0310d7ce2bca6a80cd3b0d53a1103b4dc1fa8c32 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 8 Jul 2025 02:16:29 +0000
Subject: [PATCH 22/35] chore(internal): codegen related update
---
requirements-dev.lock | 2 +-
requirements.lock | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 7ccc541..f3228a1 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -56,7 +56,7 @@ httpx==0.28.1
# via contextual-client
# via httpx-aiohttp
# via respx
-httpx-aiohttp==0.1.6
+httpx-aiohttp==0.1.8
# via contextual-client
idna==3.4
# via anyio
diff --git a/requirements.lock b/requirements.lock
index f69333d..2c03be1 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -43,7 +43,7 @@ httpcore==1.0.2
httpx==0.28.1
# via contextual-client
# via httpx-aiohttp
-httpx-aiohttp==0.1.6
+httpx-aiohttp==0.1.8
# via contextual-client
idna==3.4
# via anyio
From f0aca79b109176c6a83b31434ccdbc30e58f059d Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 9 Jul 2025 02:33:10 +0000
Subject: [PATCH 23/35] chore(internal): bump pinned h11 dep
---
requirements-dev.lock | 4 ++--
requirements.lock | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/requirements-dev.lock b/requirements-dev.lock
index f3228a1..1a6388b 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -48,9 +48,9 @@ filelock==3.12.4
frozenlist==1.6.2
# via aiohttp
# via aiosignal
-h11==0.14.0
+h11==0.16.0
# via httpcore
-httpcore==1.0.2
+httpcore==1.0.9
# via httpx
httpx==0.28.1
# via contextual-client
diff --git a/requirements.lock b/requirements.lock
index 2c03be1..321707d 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -36,9 +36,9 @@ exceptiongroup==1.2.2
frozenlist==1.6.2
# via aiohttp
# via aiosignal
-h11==0.14.0
+h11==0.16.0
# via httpcore
-httpcore==1.0.2
+httpcore==1.0.9
# via httpx
httpx==0.28.1
# via contextual-client
From f37217ff20d84d47c9adaf89c14151075e329972 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 9 Jul 2025 02:53:23 +0000
Subject: [PATCH 24/35] chore(package): mark python 3.13 as supported
---
pyproject.toml | 1 +
1 file changed, 1 insertion(+)
diff --git a/pyproject.toml b/pyproject.toml
index 51667fd..5b36ef7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -24,6 +24,7 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
"Operating System :: OS Independent",
"Operating System :: POSIX",
"Operating System :: MacOS",
From 130f4c17f8fbf89a42fa1709d6e4b4a8b36c4036 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 10 Jul 2025 02:47:43 +0000
Subject: [PATCH 25/35] fix(parsing): correctly handle nested discriminated
unions
---
src/contextual/_models.py | 13 ++++++-----
tests/test_models.py | 45 +++++++++++++++++++++++++++++++++++++++
2 files changed, 53 insertions(+), 5 deletions(-)
diff --git a/src/contextual/_models.py b/src/contextual/_models.py
index 4f21498..528d568 100644
--- a/src/contextual/_models.py
+++ b/src/contextual/_models.py
@@ -2,9 +2,10 @@
import os
import inspect
-from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast
+from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast
from datetime import date, datetime
from typing_extensions import (
+ List,
Unpack,
Literal,
ClassVar,
@@ -366,7 +367,7 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object:
if type_ is None:
raise RuntimeError(f"Unexpected field type is None for {key}")
- return construct_type(value=value, type_=type_)
+ return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None))
def is_basemodel(type_: type) -> bool:
@@ -420,7 +421,7 @@ def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T:
return cast(_T, construct_type(value=value, type_=type_))
-def construct_type(*, value: object, type_: object) -> object:
+def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]] = None) -> object:
"""Loose coercion to the expected type with construction of nested values.
If the given value does not match the expected type then it is returned as-is.
@@ -438,8 +439,10 @@ def construct_type(*, value: object, type_: object) -> object:
type_ = type_.__value__ # type: ignore[unreachable]
# unwrap `Annotated[T, ...]` -> `T`
- if is_annotated_type(type_):
- meta: tuple[Any, ...] = get_args(type_)[1:]
+ if metadata is not None:
+ meta: tuple[Any, ...] = tuple(metadata)
+ elif is_annotated_type(type_):
+ meta = get_args(type_)[1:]
type_ = extract_type_arg(type_, 0)
else:
meta = tuple()
diff --git a/tests/test_models.py b/tests/test_models.py
index 5adfed9..2a573c7 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -889,3 +889,48 @@ class ModelB(BaseModel):
)
assert isinstance(m, ModelB)
+
+
+def test_nested_discriminated_union() -> None:
+ class InnerType1(BaseModel):
+ type: Literal["type_1"]
+
+ class InnerModel(BaseModel):
+ inner_value: str
+
+ class InnerType2(BaseModel):
+ type: Literal["type_2"]
+ some_inner_model: InnerModel
+
+ class Type1(BaseModel):
+ base_type: Literal["base_type_1"]
+ value: Annotated[
+ Union[
+ InnerType1,
+ InnerType2,
+ ],
+ PropertyInfo(discriminator="type"),
+ ]
+
+ class Type2(BaseModel):
+ base_type: Literal["base_type_2"]
+
+ T = Annotated[
+ Union[
+ Type1,
+ Type2,
+ ],
+ PropertyInfo(discriminator="base_type"),
+ ]
+
+ model = construct_type(
+ type_=T,
+ value={
+ "base_type": "base_type_1",
+ "value": {
+ "type": "type_2",
+ },
+ },
+ )
+ assert isinstance(model, Type1)
+ assert isinstance(model.value, InnerType2)
From 5857ef3c8252e39ab66b1dea3e035580d0f2f006 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 11 Jul 2025 03:19:29 +0000
Subject: [PATCH 26/35] chore(readme): fix version rendering on pypi
---
README.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 19b3884..e7083e2 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
# Contextual AI Python API library
-[![PyPI version]()](https://pypi.org/project/contextual-client/)
+
+[)](https://pypi.org/project/contextual-client/)
The Contextual AI Python library provides convenient access to the Contextual AI REST API from any Python 3.8+
application. The library includes type definitions for all request params and response fields,
From 1ba6bcc49090112b3ec0dc9a0b1f5c2b487e378e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 12 Jul 2025 02:13:02 +0000
Subject: [PATCH 27/35] fix(client): don't send Content-Type header on GET
requests
---
pyproject.toml | 2 +-
src/contextual/_base_client.py | 11 +++++++++--
tests/test_client.py | 4 ++--
3 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 5b36ef7..2433533 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -39,7 +39,7 @@ Homepage = "https://github.com/ContextualAI/contextual-client-python"
Repository = "https://github.com/ContextualAI/contextual-client-python"
[project.optional-dependencies]
-aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.6"]
+aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.8"]
[tool.rye]
managed = true
diff --git a/src/contextual/_base_client.py b/src/contextual/_base_client.py
index fb8a539..afe0b3f 100644
--- a/src/contextual/_base_client.py
+++ b/src/contextual/_base_client.py
@@ -529,6 +529,15 @@ def _build_request(
# work around https://github.com/encode/httpx/discussions/2880
kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")}
+ is_body_allowed = options.method.lower() != "get"
+
+ if is_body_allowed:
+ kwargs["json"] = json_data if is_given(json_data) else None
+ kwargs["files"] = files
+ else:
+ headers.pop("Content-Type", None)
+ kwargs.pop("data", None)
+
# TODO: report this error to httpx
return self._client.build_request( # pyright: ignore[reportUnknownMemberType]
headers=headers,
@@ -540,8 +549,6 @@ def _build_request(
# 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,
- json=json_data if is_given(json_data) else None,
- files=files,
**kwargs,
)
diff --git a/tests/test_client.py b/tests/test_client.py
index 15a6d55..df3fc8d 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -464,7 +464,7 @@ def test_request_extra_query(self) -> None:
def test_multipart_repeating_array(self, client: ContextualAI) -> None:
request = client._build_request(
FinalRequestOptions.construct(
- method="get",
+ method="post",
url="/foo",
headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"},
json_data={"array": ["foo", "bar"]},
@@ -1275,7 +1275,7 @@ def test_request_extra_query(self) -> None:
def test_multipart_repeating_array(self, async_client: AsyncContextualAI) -> None:
request = async_client._build_request(
FinalRequestOptions.construct(
- method="get",
+ method="post",
url="/foo",
headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"},
json_data={"array": ["foo", "bar"]},
From 5aacfd73cd62e9440b927c74e29bd4ee03766334 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 15 Jul 2025 02:12:26 +0000
Subject: [PATCH 28/35] feat: clean up environment call outs
---
README.md | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/README.md b/README.md
index e7083e2..c05a737 100644
--- a/README.md
+++ b/README.md
@@ -81,7 +81,6 @@ pip install contextual-client[aiohttp]
Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`:
```python
-import os
import asyncio
from contextual import DefaultAioHttpClient
from contextual import AsyncContextualAI
@@ -89,7 +88,7 @@ from contextual import AsyncContextualAI
async def main() -> None:
async with AsyncContextualAI(
- api_key=os.environ.get("CONTEXTUAL_API_KEY"), # This is the default and can be omitted
+ api_key="My API Key",
http_client=DefaultAioHttpClient(),
) as client:
create_agent_output = await client.agents.create(
From a81e19084356382c7b709215b1462e099d56f2a6 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 22 Jul 2025 02:15:04 +0000
Subject: [PATCH 29/35] fix(parsing): ignore empty metadata
---
src/contextual/_models.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/contextual/_models.py b/src/contextual/_models.py
index 528d568..ffcbf67 100644
--- a/src/contextual/_models.py
+++ b/src/contextual/_models.py
@@ -439,7 +439,7 @@ def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]
type_ = type_.__value__ # type: ignore[unreachable]
# unwrap `Annotated[T, ...]` -> `T`
- if metadata is not None:
+ if metadata is not None and len(metadata) > 0:
meta: tuple[Any, ...] = tuple(metadata)
elif is_annotated_type(type_):
meta = get_args(type_)[1:]
From 89f10b3a97483b99e0ec06a346286619faec5c12 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 23 Jul 2025 02:17:42 +0000
Subject: [PATCH 30/35] fix(parsing): parse extra field types
---
src/contextual/_models.py | 25 +++++++++++++++++++++++--
tests/test_models.py | 29 ++++++++++++++++++++++++++++-
2 files changed, 51 insertions(+), 3 deletions(-)
diff --git a/src/contextual/_models.py b/src/contextual/_models.py
index ffcbf67..b8387ce 100644
--- a/src/contextual/_models.py
+++ b/src/contextual/_models.py
@@ -208,14 +208,18 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride]
else:
fields_values[name] = field_get_default(field)
+ extra_field_type = _get_extra_fields_type(__cls)
+
_extra = {}
for key, value in values.items():
if key not in model_fields:
+ parsed = construct_type(value=value, type_=extra_field_type) if extra_field_type is not None else value
+
if PYDANTIC_V2:
- _extra[key] = value
+ _extra[key] = parsed
else:
_fields_set.add(key)
- fields_values[key] = value
+ fields_values[key] = parsed
object.__setattr__(m, "__dict__", fields_values)
@@ -370,6 +374,23 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object:
return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None))
+def _get_extra_fields_type(cls: type[pydantic.BaseModel]) -> type | None:
+ if not PYDANTIC_V2:
+ # TODO
+ return None
+
+ schema = cls.__pydantic_core_schema__
+ if schema["type"] == "model":
+ fields = schema["schema"]
+ if fields["type"] == "model-fields":
+ extras = fields.get("extras_schema")
+ if extras and "cls" in extras:
+ # mypy can't narrow the type
+ return extras["cls"] # type: ignore[no-any-return]
+
+ return None
+
+
def is_basemodel(type_: type) -> bool:
"""Returns whether or not the given type is either a `BaseModel` or a union of `BaseModel`"""
if is_union(type_):
diff --git a/tests/test_models.py b/tests/test_models.py
index 2a573c7..ae4b3f0 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -1,5 +1,5 @@
import json
-from typing import Any, Dict, List, Union, Optional, cast
+from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional, cast
from datetime import datetime, timezone
from typing_extensions import Literal, Annotated, TypeAliasType
@@ -934,3 +934,30 @@ class Type2(BaseModel):
)
assert isinstance(model, Type1)
assert isinstance(model.value, InnerType2)
+
+
+@pytest.mark.skipif(not PYDANTIC_V2, reason="this is only supported in pydantic v2 for now")
+def test_extra_properties() -> None:
+ class Item(BaseModel):
+ prop: int
+
+ class Model(BaseModel):
+ __pydantic_extra__: Dict[str, Item] = Field(init=False) # pyright: ignore[reportIncompatibleVariableOverride]
+
+ other: str
+
+ if TYPE_CHECKING:
+
+ def __getattr__(self, attr: str) -> Item: ...
+
+ model = construct_type(
+ type_=Model,
+ value={
+ "a": {"prop": 1},
+ "other": "foo",
+ },
+ )
+ assert isinstance(model, Model)
+ assert model.a.prop == 1
+ assert isinstance(model.a, Item)
+ assert model.other == "foo"
From 77265c18261b46255146f4a0fd82e2aae41ae160 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 25 Jul 2025 04:24:01 +0000
Subject: [PATCH 31/35] chore(project): add settings file for vscode
---
.gitignore | 1 -
.vscode/settings.json | 3 +++
2 files changed, 3 insertions(+), 1 deletion(-)
create mode 100644 .vscode/settings.json
diff --git a/.gitignore b/.gitignore
index 5ef86bd..4aeb2ab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,4 @@
.prism.log
-.vscode
_dev
__pycache__
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..5b01030
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "python.analysis.importFormat": "relative",
+}
From 44d064d3013ef31ec6cb709682ab5fef4d2ed531 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 31 Jul 2025 05:59:46 +0000
Subject: [PATCH 32/35] feat(client): support file upload requests
---
src/contextual/_base_client.py | 5 ++++-
src/contextual/_files.py | 8 ++++----
2 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/src/contextual/_base_client.py b/src/contextual/_base_client.py
index afe0b3f..2d5b5fa 100644
--- a/src/contextual/_base_client.py
+++ b/src/contextual/_base_client.py
@@ -532,7 +532,10 @@ def _build_request(
is_body_allowed = options.method.lower() != "get"
if is_body_allowed:
- kwargs["json"] = json_data if is_given(json_data) else None
+ if isinstance(json_data, bytes):
+ kwargs["content"] = json_data
+ else:
+ kwargs["json"] = json_data if is_given(json_data) else None
kwargs["files"] = files
else:
headers.pop("Content-Type", None)
diff --git a/src/contextual/_files.py b/src/contextual/_files.py
index 8c48fd6..3a712ec 100644
--- a/src/contextual/_files.py
+++ b/src/contextual/_files.py
@@ -69,12 +69,12 @@ def _transform_file(file: FileTypes) -> HttpxFileTypes:
return file
if is_tuple_t(file):
- return (file[0], _read_file_content(file[1]), *file[2:])
+ return (file[0], read_file_content(file[1]), *file[2:])
raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple")
-def _read_file_content(file: FileContent) -> HttpxFileContent:
+def read_file_content(file: FileContent) -> HttpxFileContent:
if isinstance(file, os.PathLike):
return pathlib.Path(file).read_bytes()
return file
@@ -111,12 +111,12 @@ async def _async_transform_file(file: FileTypes) -> HttpxFileTypes:
return file
if is_tuple_t(file):
- return (file[0], await _async_read_file_content(file[1]), *file[2:])
+ return (file[0], await async_read_file_content(file[1]), *file[2:])
raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple")
-async def _async_read_file_content(file: FileContent) -> HttpxFileContent:
+async def async_read_file_content(file: FileContent) -> HttpxFileContent:
if isinstance(file, os.PathLike):
return await anyio.Path(file).read_bytes()
From 40379a3d51aef12b1a0264e515ac145c91e41644 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 1 Aug 2025 04:35:31 +0000
Subject: [PATCH 33/35] chore(internal): update examples
---
tests/api_resources/agents/test_query.py | 16 ++--------------
1 file changed, 2 insertions(+), 14 deletions(-)
diff --git a/tests/api_resources/agents/test_query.py b/tests/api_resources/agents/test_query.py
index e67f949..96f5da6 100644
--- a/tests/api_resources/agents/test_query.py
+++ b/tests/api_resources/agents/test_query.py
@@ -49,13 +49,7 @@ def test_method_create_with_all_params(self, client: ContextualAI) -> None:
retrievals_only=True,
conversation_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
documents_filters={
- "filters": [
- {
- "field": "field1",
- "operator": "equals",
- "value": "value1",
- }
- ],
+ "filters": [],
"operator": "AND",
},
llm_model_id="llm_model_id",
@@ -310,13 +304,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncContextual
retrievals_only=True,
conversation_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
documents_filters={
- "filters": [
- {
- "field": "field1",
- "operator": "equals",
- "value": "value1",
- }
- ],
+ "filters": [],
"operator": "AND",
},
llm_model_id="llm_model_id",
From 465af9ec69d6456078fb4137b39d1dd33a3f60b2 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 6 Aug 2025 07:30:44 +0000
Subject: [PATCH 34/35] chore(internal): fix ruff target version
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index 2433533..604435c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -159,7 +159,7 @@ reportPrivateUsage = false
[tool.ruff]
line-length = 120
output-format = "grouped"
-target-version = "py37"
+target-version = "py38"
[tool.ruff.format]
docstring-code-format = true
From ecfa79a03fd7966d0642fd1aa9827056ebd9c85a Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 6 Aug 2025 07:31:30 +0000
Subject: [PATCH 35/35] release: 0.8.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 53 +++++++++++++++++++++++++++++++++++
pyproject.toml | 2 +-
src/contextual/_version.py | 2 +-
4 files changed, 56 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 1b77f50..6538ca9 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.7.0"
+ ".": "0.8.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a2efd9b..8a8f485 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,58 @@
# Changelog
+## 0.8.0 (2025-08-06)
+
+Full Changelog: [v0.7.0...v0.8.0](https://github.com/ContextualAI/contextual-client-python/compare/v0.7.0...v0.8.0)
+
+### Features
+
+* clean up environment call outs ([5aacfd7](https://github.com/ContextualAI/contextual-client-python/commit/5aacfd73cd62e9440b927c74e29bd4ee03766334))
+* **client:** add follow_redirects request option ([35e7c78](https://github.com/ContextualAI/contextual-client-python/commit/35e7c78c7d1801a0afe4d73bbff3e7c695f5f19f))
+* **client:** add support for aiohttp ([d54f53c](https://github.com/ContextualAI/contextual-client-python/commit/d54f53cfa0878acbad344622f7aae1b2e939ae1c))
+* **client:** support file upload requests ([44d064d](https://github.com/ContextualAI/contextual-client-python/commit/44d064d3013ef31ec6cb709682ab5fef4d2ed531))
+
+
+### Bug Fixes
+
+* **ci:** correct conditional ([0e1ab57](https://github.com/ContextualAI/contextual-client-python/commit/0e1ab57132d5a038aac790b463166200ae436fc3))
+* **ci:** release-doctor — report correct token name ([ce0af3b](https://github.com/ContextualAI/contextual-client-python/commit/ce0af3be8b2f90af2bc4e38979a801df1e98e989))
+* **client:** correctly parse binary response | stream ([518cbab](https://github.com/ContextualAI/contextual-client-python/commit/518cbabda3ce7f53721c0fc916ae89706899a4ec))
+* **client:** don't send Content-Type header on GET requests ([1ba6bcc](https://github.com/ContextualAI/contextual-client-python/commit/1ba6bcc49090112b3ec0dc9a0b1f5c2b487e378e))
+* **docs/api:** remove references to nonexistent types ([9fd7133](https://github.com/ContextualAI/contextual-client-python/commit/9fd7133c6748ba1b1676a674da35d57f02f01a86))
+* **parsing:** correctly handle nested discriminated unions ([130f4c1](https://github.com/ContextualAI/contextual-client-python/commit/130f4c17f8fbf89a42fa1709d6e4b4a8b36c4036))
+* **parsing:** ignore empty metadata ([a81e190](https://github.com/ContextualAI/contextual-client-python/commit/a81e19084356382c7b709215b1462e099d56f2a6))
+* **parsing:** parse extra field types ([89f10b3](https://github.com/ContextualAI/contextual-client-python/commit/89f10b3a97483b99e0ec06a346286619faec5c12))
+* **tests:** fix: tests which call HTTP endpoints directly with the example parameters ([d7920f1](https://github.com/ContextualAI/contextual-client-python/commit/d7920f111d6175e6714482918e34992fb51739d9))
+
+
+### Chores
+
+* **ci:** change upload type ([f72dfb7](https://github.com/ContextualAI/contextual-client-python/commit/f72dfb77ff1fcae80efa5b286800ed77af6d0889))
+* **ci:** enable for pull requests ([84fbba4](https://github.com/ContextualAI/contextual-client-python/commit/84fbba4c22dbbf8517841c7961a37dba246126dc))
+* **ci:** fix installation instructions ([f191464](https://github.com/ContextualAI/contextual-client-python/commit/f191464e75f48395e76d6007712ae8548268b45f))
+* **ci:** only run for pushes and fork pull requests ([b9520a0](https://github.com/ContextualAI/contextual-client-python/commit/b9520a0ad9c16d3ad0386ce70a15df4191751364))
+* **ci:** upload sdks to package manager ([1f04b9e](https://github.com/ContextualAI/contextual-client-python/commit/1f04b9ecca3a4a3d2235c5cfa21bd9b36a358754))
+* **docs:** grammar improvements ([01370fb](https://github.com/ContextualAI/contextual-client-python/commit/01370fb62278f1def879352910c2520102c89993))
+* **docs:** remove reference to rye shell ([68f70a8](https://github.com/ContextualAI/contextual-client-python/commit/68f70a88e5b45773140c4b4a02c0506f3d078ad9))
+* **docs:** remove unnecessary param examples ([f603dcd](https://github.com/ContextualAI/contextual-client-python/commit/f603dcdd966c77ce3e8b8dba8e878eb273ef1688))
+* **internal:** bump pinned h11 dep ([f0aca79](https://github.com/ContextualAI/contextual-client-python/commit/f0aca79b109176c6a83b31434ccdbc30e58f059d))
+* **internal:** codegen related update ([0310d7c](https://github.com/ContextualAI/contextual-client-python/commit/0310d7ce2bca6a80cd3b0d53a1103b4dc1fa8c32))
+* **internal:** fix ruff target version ([465af9e](https://github.com/ContextualAI/contextual-client-python/commit/465af9ec69d6456078fb4137b39d1dd33a3f60b2))
+* **internal:** update conftest.py ([b324ed3](https://github.com/ContextualAI/contextual-client-python/commit/b324ed373c9c174a44eb52dc6d2384e82c0af4b8))
+* **internal:** update examples ([40379a3](https://github.com/ContextualAI/contextual-client-python/commit/40379a3d51aef12b1a0264e515ac145c91e41644))
+* **package:** mark python 3.13 as supported ([f37217f](https://github.com/ContextualAI/contextual-client-python/commit/f37217ff20d84d47c9adaf89c14151075e329972))
+* **project:** add settings file for vscode ([77265c1](https://github.com/ContextualAI/contextual-client-python/commit/77265c18261b46255146f4a0fd82e2aae41ae160))
+* **readme:** fix version rendering on pypi ([5857ef3](https://github.com/ContextualAI/contextual-client-python/commit/5857ef3c8252e39ab66b1dea3e035580d0f2f006))
+* **readme:** update badges ([b747f45](https://github.com/ContextualAI/contextual-client-python/commit/b747f452ab31df0805dd07a516fe63c460353c57))
+* **tests:** add tests for httpx client instantiation & proxies ([0c4973f](https://github.com/ContextualAI/contextual-client-python/commit/0c4973fed123a77a16b189439b3f4976fcc91770))
+* **tests:** run tests in parallel ([f75c912](https://github.com/ContextualAI/contextual-client-python/commit/f75c912ff643028317dde5fb0dfd08470b26ac29))
+* **tests:** skip some failing tests on the latest python versions ([dd32830](https://github.com/ContextualAI/contextual-client-python/commit/dd32830a8266dbf736c85285ec611854659511e7))
+
+
+### Documentation
+
+* **client:** fix httpx.Timeout documentation reference ([3517a3d](https://github.com/ContextualAI/contextual-client-python/commit/3517a3d02c7447c027bc82baf3a83333eb3c9b55))
+
## 0.7.0 (2025-05-13)
Full Changelog: [v0.6.0...v0.7.0](https://github.com/ContextualAI/contextual-client-python/compare/v0.6.0...v0.7.0)
diff --git a/pyproject.toml b/pyproject.toml
index 604435c..1169a3e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "contextual-client"
-version = "0.7.0"
+version = "0.8.0"
description = "The official Python library for the Contextual AI API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/contextual/_version.py b/src/contextual/_version.py
index 99bb927..a855209 100644
--- a/src/contextual/_version.py
+++ b/src/contextual/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "contextual"
-__version__ = "0.7.0" # x-release-please-version
+__version__ = "0.8.0" # x-release-please-version