From 4b1fb3df6082f33de48374523712e9cd0dbbb2a3 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 14 Oct 2025 02:32:03 +0000
Subject: [PATCH 01/28] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 06232146..88b12225 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 20
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-cd8c28747615d8967e4e20e6fd5b9488a3022fece37f1c4c133c9b8d9c4415f3.yml
-openapi_spec_hash: 2aa6c5d6faa2cbd4038108b5ebc103b3
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-5c32c854b3a68ddee47fb9f4ae077ac9df93d9ecb46311282285e9ff56f979be.yml
+openapi_spec_hash: eb7f5be1d2520355ad5657607c1c03d2
config_hash: e29127278ff246754ce4801403db0cd9
From 1bfef28fdcd09c6b6cccfb0aa21055b2ad220403 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 14 Oct 2025 06:32:05 +0000
Subject: [PATCH 02/28] feat(api): api update
---
.stats.yml | 6 +-
api.md | 7 +-
.../resources/integrations/integrations.py | 81 ------------------
src/hyperspell/types/__init__.py | 1 -
src/hyperspell/types/auth_me_response.py | 63 +-------------
.../types/integration_revoke_response.py | 11 ---
tests/api_resources/test_integrations.py | 82 +------------------
7 files changed, 6 insertions(+), 245 deletions(-)
delete mode 100644 src/hyperspell/types/integration_revoke_response.py
diff --git a/.stats.yml b/.stats.yml
index 88b12225..5d4d93fb 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 20
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-5c32c854b3a68ddee47fb9f4ae077ac9df93d9ecb46311282285e9ff56f979be.yml
-openapi_spec_hash: eb7f5be1d2520355ad5657607c1c03d2
+configured_endpoints: 19
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-f6ce5154226fb08b6df06b813c46950bf10835af91a7e7b64dd54fc077e35dcc.yml
+openapi_spec_hash: f63ed123d263784f1e9971061494f730
config_hash: e29127278ff246754ce4801403db0cd9
diff --git a/api.md b/api.md
index a0af7c08..e8a1859f 100644
--- a/api.md
+++ b/api.md
@@ -9,18 +9,13 @@ from hyperspell.types import QueryResult
Types:
```python
-from hyperspell.types import (
- IntegrationListResponse,
- IntegrationConnectResponse,
- IntegrationRevokeResponse,
-)
+from hyperspell.types import IntegrationListResponse, IntegrationConnectResponse
```
Methods:
- client.integrations.list() -> IntegrationListResponse
- client.integrations.connect(integration_id, \*\*params) -> IntegrationConnectResponse
-- client.integrations.revoke(integration_id) -> IntegrationRevokeResponse
## GoogleCalendar
diff --git a/src/hyperspell/resources/integrations/integrations.py b/src/hyperspell/resources/integrations/integrations.py
index 178d7b9a..09eb99a0 100644
--- a/src/hyperspell/resources/integrations/integrations.py
+++ b/src/hyperspell/resources/integrations/integrations.py
@@ -43,7 +43,6 @@
AsyncGoogleCalendarResourceWithStreamingResponse,
)
from ...types.integration_list_response import IntegrationListResponse
-from ...types.integration_revoke_response import IntegrationRevokeResponse
from ...types.integration_connect_response import IntegrationConnectResponse
__all__ = ["IntegrationsResource", "AsyncIntegrationsResource"]
@@ -140,40 +139,6 @@ def connect(
cast_to=IntegrationConnectResponse,
)
- def revoke(
- self,
- integration_id: str,
- *,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> IntegrationRevokeResponse:
- """
- Revokes Hyperspell's access the given provider and deletes all stored
- credentials and indexed data.
-
- Args:
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- if not integration_id:
- raise ValueError(f"Expected a non-empty value for `integration_id` but received {integration_id!r}")
- return self._get(
- f"/integrations/{integration_id}/revoke",
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=IntegrationRevokeResponse,
- )
-
class AsyncIntegrationsResource(AsyncAPIResource):
@cached_property
@@ -266,40 +231,6 @@ async def connect(
cast_to=IntegrationConnectResponse,
)
- async def revoke(
- self,
- integration_id: str,
- *,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> IntegrationRevokeResponse:
- """
- Revokes Hyperspell's access the given provider and deletes all stored
- credentials and indexed data.
-
- Args:
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- if not integration_id:
- raise ValueError(f"Expected a non-empty value for `integration_id` but received {integration_id!r}")
- return await self._get(
- f"/integrations/{integration_id}/revoke",
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=IntegrationRevokeResponse,
- )
-
class IntegrationsResourceWithRawResponse:
def __init__(self, integrations: IntegrationsResource) -> None:
@@ -311,9 +242,6 @@ def __init__(self, integrations: IntegrationsResource) -> None:
self.connect = to_raw_response_wrapper(
integrations.connect,
)
- self.revoke = to_raw_response_wrapper(
- integrations.revoke,
- )
@cached_property
def google_calendar(self) -> GoogleCalendarResourceWithRawResponse:
@@ -338,9 +266,6 @@ def __init__(self, integrations: AsyncIntegrationsResource) -> None:
self.connect = async_to_raw_response_wrapper(
integrations.connect,
)
- self.revoke = async_to_raw_response_wrapper(
- integrations.revoke,
- )
@cached_property
def google_calendar(self) -> AsyncGoogleCalendarResourceWithRawResponse:
@@ -365,9 +290,6 @@ def __init__(self, integrations: IntegrationsResource) -> None:
self.connect = to_streamed_response_wrapper(
integrations.connect,
)
- self.revoke = to_streamed_response_wrapper(
- integrations.revoke,
- )
@cached_property
def google_calendar(self) -> GoogleCalendarResourceWithStreamingResponse:
@@ -392,9 +314,6 @@ def __init__(self, integrations: AsyncIntegrationsResource) -> None:
self.connect = async_to_streamed_response_wrapper(
integrations.connect,
)
- self.revoke = async_to_streamed_response_wrapper(
- integrations.revoke,
- )
@cached_property
def google_calendar(self) -> AsyncGoogleCalendarResourceWithStreamingResponse:
diff --git a/src/hyperspell/types/__init__.py b/src/hyperspell/types/__init__.py
index 5f3077cf..7764658d 100644
--- a/src/hyperspell/types/__init__.py
+++ b/src/hyperspell/types/__init__.py
@@ -20,7 +20,6 @@
from .integration_list_response import IntegrationListResponse as IntegrationListResponse
from .integration_connect_params import IntegrationConnectParams as IntegrationConnectParams
from .evaluate_score_query_params import EvaluateScoreQueryParams as EvaluateScoreQueryParams
-from .integration_revoke_response import IntegrationRevokeResponse as IntegrationRevokeResponse
from .integration_connect_response import IntegrationConnectResponse as IntegrationConnectResponse
from .evaluate_score_query_response import EvaluateScoreQueryResponse as EvaluateScoreQueryResponse
from .evaluate_score_highlight_params import EvaluateScoreHighlightParams as EvaluateScoreHighlightParams
diff --git a/src/hyperspell/types/auth_me_response.py b/src/hyperspell/types/auth_me_response.py
index 7eb70095..d4ca2c54 100644
--- a/src/hyperspell/types/auth_me_response.py
+++ b/src/hyperspell/types/auth_me_response.py
@@ -6,7 +6,7 @@
from .._models import BaseModel
-__all__ = ["AuthMeResponse", "App", "Connection"]
+__all__ = ["AuthMeResponse", "App"]
class App(BaseModel):
@@ -23,64 +23,6 @@ class App(BaseModel):
"""The app's redirect URL"""
-class Connection(BaseModel):
- id: str
- """The connection's id"""
-
- label: Optional[str] = None
- """The connection's label"""
-
- provider: Literal[
- "collections",
- "vault",
- "web_crawler",
- "notion",
- "slack",
- "google_calendar",
- "reddit",
- "box",
- "google_drive",
- "airtable",
- "algolia",
- "amplitude",
- "asana",
- "ashby",
- "bamboohr",
- "basecamp",
- "bubbles",
- "calendly",
- "confluence",
- "clickup",
- "datadog",
- "deel",
- "discord",
- "dropbox",
- "exa",
- "facebook",
- "front",
- "github",
- "gitlab",
- "google_docs",
- "google_mail",
- "google_sheet",
- "hubspot",
- "jira",
- "linear",
- "microsoft_teams",
- "mixpanel",
- "monday",
- "outlook",
- "perplexity",
- "rippling",
- "salesforce",
- "segment",
- "todoist",
- "twitter",
- "zoom",
- ]
- """The connection's provider"""
-
-
class AuthMeResponse(BaseModel):
id: str
"""The user's id"""
@@ -140,9 +82,6 @@ class AuthMeResponse(BaseModel):
]
"""All integrations available for the app"""
- connections: List[Connection]
- """Established connections for the user"""
-
installed_integrations: List[
Literal[
"collections",
diff --git a/src/hyperspell/types/integration_revoke_response.py b/src/hyperspell/types/integration_revoke_response.py
deleted file mode 100644
index 2385ca96..00000000
--- a/src/hyperspell/types/integration_revoke_response.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from .._models import BaseModel
-
-__all__ = ["IntegrationRevokeResponse"]
-
-
-class IntegrationRevokeResponse(BaseModel):
- message: str
-
- success: bool
diff --git a/tests/api_resources/test_integrations.py b/tests/api_resources/test_integrations.py
index 3b2934d7..ced89a16 100644
--- a/tests/api_resources/test_integrations.py
+++ b/tests/api_resources/test_integrations.py
@@ -9,11 +9,7 @@
from hyperspell import Hyperspell, AsyncHyperspell
from tests.utils import assert_matches_type
-from hyperspell.types import (
- IntegrationListResponse,
- IntegrationRevokeResponse,
- IntegrationConnectResponse,
-)
+from hyperspell.types import IntegrationListResponse, IntegrationConnectResponse
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -92,44 +88,6 @@ def test_path_params_connect(self, client: Hyperspell) -> None:
integration_id="",
)
- @parametrize
- def test_method_revoke(self, client: Hyperspell) -> None:
- integration = client.integrations.revoke(
- "integration_id",
- )
- assert_matches_type(IntegrationRevokeResponse, integration, path=["response"])
-
- @parametrize
- def test_raw_response_revoke(self, client: Hyperspell) -> None:
- response = client.integrations.with_raw_response.revoke(
- "integration_id",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- integration = response.parse()
- assert_matches_type(IntegrationRevokeResponse, integration, path=["response"])
-
- @parametrize
- def test_streaming_response_revoke(self, client: Hyperspell) -> None:
- with client.integrations.with_streaming_response.revoke(
- "integration_id",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- integration = response.parse()
- assert_matches_type(IntegrationRevokeResponse, integration, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @parametrize
- def test_path_params_revoke(self, client: Hyperspell) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `integration_id` but received ''"):
- client.integrations.with_raw_response.revoke(
- "",
- )
-
class TestAsyncIntegrations:
parametrize = pytest.mark.parametrize(
@@ -206,41 +164,3 @@ async def test_path_params_connect(self, async_client: AsyncHyperspell) -> None:
await async_client.integrations.with_raw_response.connect(
integration_id="",
)
-
- @parametrize
- async def test_method_revoke(self, async_client: AsyncHyperspell) -> None:
- integration = await async_client.integrations.revoke(
- "integration_id",
- )
- assert_matches_type(IntegrationRevokeResponse, integration, path=["response"])
-
- @parametrize
- async def test_raw_response_revoke(self, async_client: AsyncHyperspell) -> None:
- response = await async_client.integrations.with_raw_response.revoke(
- "integration_id",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- integration = await response.parse()
- assert_matches_type(IntegrationRevokeResponse, integration, path=["response"])
-
- @parametrize
- async def test_streaming_response_revoke(self, async_client: AsyncHyperspell) -> None:
- async with async_client.integrations.with_streaming_response.revoke(
- "integration_id",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- integration = await response.parse()
- assert_matches_type(IntegrationRevokeResponse, integration, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @parametrize
- async def test_path_params_revoke(self, async_client: AsyncHyperspell) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `integration_id` but received ''"):
- await async_client.integrations.with_raw_response.revoke(
- "",
- )
From e8d4554de53dd5986945a613173599ef447f6d48 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 16 Oct 2025 22:18:09 +0000
Subject: [PATCH 03/28] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index 5d4d93fb..a6990b43 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 19
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-f6ce5154226fb08b6df06b813c46950bf10835af91a7e7b64dd54fc077e35dcc.yml
openapi_spec_hash: f63ed123d263784f1e9971061494f730
-config_hash: e29127278ff246754ce4801403db0cd9
+config_hash: c8e8f13e0a3a2b0acb3021e7925abb1f
From 32ac22aa9975c306aae92d97e3c8fc7103c3b9bd Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 16 Oct 2025 22:18:48 +0000
Subject: [PATCH 04/28] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index a6990b43..661e2c5f 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 19
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-f6ce5154226fb08b6df06b813c46950bf10835af91a7e7b64dd54fc077e35dcc.yml
openapi_spec_hash: f63ed123d263784f1e9971061494f730
-config_hash: c8e8f13e0a3a2b0acb3021e7925abb1f
+config_hash: 74aab95580dcb90ce656856c30095dfa
From 49e68c866483f9be8900eef7fc3e776e43cd74b1 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 17 Oct 2025 01:45:36 +0000
Subject: [PATCH 05/28] feat(api): update via SDK Studio
---
.stats.yml | 4 +-
api.md | 13 ++
src/hyperspell/_client.py | 10 +-
src/hyperspell/resources/__init__.py | 14 ++
src/hyperspell/resources/connections.py | 216 ++++++++++++++++++
src/hyperspell/types/__init__.py | 2 +
.../types/connection_list_response.py | 70 ++++++
.../types/connection_revoke_response.py | 11 +
tests/api_resources/test_connections.py | 150 ++++++++++++
9 files changed, 487 insertions(+), 3 deletions(-)
create mode 100644 src/hyperspell/resources/connections.py
create mode 100644 src/hyperspell/types/connection_list_response.py
create mode 100644 src/hyperspell/types/connection_revoke_response.py
create mode 100644 tests/api_resources/test_connections.py
diff --git a/.stats.yml b/.stats.yml
index 661e2c5f..faae7394 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 19
+configured_endpoints: 21
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-f6ce5154226fb08b6df06b813c46950bf10835af91a7e7b64dd54fc077e35dcc.yml
openapi_spec_hash: f63ed123d263784f1e9971061494f730
-config_hash: 74aab95580dcb90ce656856c30095dfa
+config_hash: ca45358b5407440488ec988e3ee21412
diff --git a/api.md b/api.md
index e8a1859f..0452defa 100644
--- a/api.md
+++ b/api.md
@@ -4,6 +4,19 @@
from hyperspell.types import QueryResult
```
+# Connections
+
+Types:
+
+```python
+from hyperspell.types import ConnectionListResponse, ConnectionRevokeResponse
+```
+
+Methods:
+
+- client.connections.list() -> ConnectionListResponse
+- client.connections.revoke(connection_id) -> ConnectionRevokeResponse
+
# Integrations
Types:
diff --git a/src/hyperspell/_client.py b/src/hyperspell/_client.py
index 3abe0ae8..a6aa68c4 100644
--- a/src/hyperspell/_client.py
+++ b/src/hyperspell/_client.py
@@ -21,7 +21,7 @@
)
from ._utils import is_given, get_async_library
from ._version import __version__
-from .resources import auth, vaults, evaluate, memories
+from .resources import auth, vaults, evaluate, memories, connections
from ._streaming import Stream as Stream, AsyncStream as AsyncStream
from ._exceptions import APIStatusError, HyperspellError
from ._base_client import (
@@ -44,6 +44,7 @@
class Hyperspell(SyncAPIClient):
+ connections: connections.ConnectionsResource
integrations: integrations.IntegrationsResource
memories: memories.MemoriesResource
evaluate: evaluate.EvaluateResource
@@ -110,6 +111,7 @@ def __init__(
_strict_response_validation=_strict_response_validation,
)
+ self.connections = connections.ConnectionsResource(self)
self.integrations = integrations.IntegrationsResource(self)
self.memories = memories.MemoriesResource(self)
self.evaluate = evaluate.EvaluateResource(self)
@@ -237,6 +239,7 @@ def _make_status_error(
class AsyncHyperspell(AsyncAPIClient):
+ connections: connections.AsyncConnectionsResource
integrations: integrations.AsyncIntegrationsResource
memories: memories.AsyncMemoriesResource
evaluate: evaluate.AsyncEvaluateResource
@@ -303,6 +306,7 @@ def __init__(
_strict_response_validation=_strict_response_validation,
)
+ self.connections = connections.AsyncConnectionsResource(self)
self.integrations = integrations.AsyncIntegrationsResource(self)
self.memories = memories.AsyncMemoriesResource(self)
self.evaluate = evaluate.AsyncEvaluateResource(self)
@@ -431,6 +435,7 @@ def _make_status_error(
class HyperspellWithRawResponse:
def __init__(self, client: Hyperspell) -> None:
+ self.connections = connections.ConnectionsResourceWithRawResponse(client.connections)
self.integrations = integrations.IntegrationsResourceWithRawResponse(client.integrations)
self.memories = memories.MemoriesResourceWithRawResponse(client.memories)
self.evaluate = evaluate.EvaluateResourceWithRawResponse(client.evaluate)
@@ -440,6 +445,7 @@ def __init__(self, client: Hyperspell) -> None:
class AsyncHyperspellWithRawResponse:
def __init__(self, client: AsyncHyperspell) -> None:
+ self.connections = connections.AsyncConnectionsResourceWithRawResponse(client.connections)
self.integrations = integrations.AsyncIntegrationsResourceWithRawResponse(client.integrations)
self.memories = memories.AsyncMemoriesResourceWithRawResponse(client.memories)
self.evaluate = evaluate.AsyncEvaluateResourceWithRawResponse(client.evaluate)
@@ -449,6 +455,7 @@ def __init__(self, client: AsyncHyperspell) -> None:
class HyperspellWithStreamedResponse:
def __init__(self, client: Hyperspell) -> None:
+ self.connections = connections.ConnectionsResourceWithStreamingResponse(client.connections)
self.integrations = integrations.IntegrationsResourceWithStreamingResponse(client.integrations)
self.memories = memories.MemoriesResourceWithStreamingResponse(client.memories)
self.evaluate = evaluate.EvaluateResourceWithStreamingResponse(client.evaluate)
@@ -458,6 +465,7 @@ def __init__(self, client: Hyperspell) -> None:
class AsyncHyperspellWithStreamedResponse:
def __init__(self, client: AsyncHyperspell) -> None:
+ self.connections = connections.AsyncConnectionsResourceWithStreamingResponse(client.connections)
self.integrations = integrations.AsyncIntegrationsResourceWithStreamingResponse(client.integrations)
self.memories = memories.AsyncMemoriesResourceWithStreamingResponse(client.memories)
self.evaluate = evaluate.AsyncEvaluateResourceWithStreamingResponse(client.evaluate)
diff --git a/src/hyperspell/resources/__init__.py b/src/hyperspell/resources/__init__.py
index d9c32c3f..085ac7c5 100644
--- a/src/hyperspell/resources/__init__.py
+++ b/src/hyperspell/resources/__init__.py
@@ -32,6 +32,14 @@
MemoriesResourceWithStreamingResponse,
AsyncMemoriesResourceWithStreamingResponse,
)
+from .connections import (
+ ConnectionsResource,
+ AsyncConnectionsResource,
+ ConnectionsResourceWithRawResponse,
+ AsyncConnectionsResourceWithRawResponse,
+ ConnectionsResourceWithStreamingResponse,
+ AsyncConnectionsResourceWithStreamingResponse,
+)
from .integrations import (
IntegrationsResource,
AsyncIntegrationsResource,
@@ -42,6 +50,12 @@
)
__all__ = [
+ "ConnectionsResource",
+ "AsyncConnectionsResource",
+ "ConnectionsResourceWithRawResponse",
+ "AsyncConnectionsResourceWithRawResponse",
+ "ConnectionsResourceWithStreamingResponse",
+ "AsyncConnectionsResourceWithStreamingResponse",
"IntegrationsResource",
"AsyncIntegrationsResource",
"IntegrationsResourceWithRawResponse",
diff --git a/src/hyperspell/resources/connections.py b/src/hyperspell/resources/connections.py
new file mode 100644
index 00000000..98799ee3
--- /dev/null
+++ b/src/hyperspell/resources/connections.py
@@ -0,0 +1,216 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import httpx
+
+from .._types import Body, Query, Headers, NotGiven, not_given
+from .._compat import cached_property
+from .._resource import SyncAPIResource, AsyncAPIResource
+from .._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from .._base_client import make_request_options
+from ..types.connection_list_response import ConnectionListResponse
+from ..types.connection_revoke_response import ConnectionRevokeResponse
+
+__all__ = ["ConnectionsResource", "AsyncConnectionsResource"]
+
+
+class ConnectionsResource(SyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> ConnectionsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/hyperspell/python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return ConnectionsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> ConnectionsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/hyperspell/python-sdk#with_streaming_response
+ """
+ return ConnectionsResourceWithStreamingResponse(self)
+
+ def list(
+ self,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ConnectionListResponse:
+ """List all connections for the user."""
+ return self._get(
+ "/connections/list",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ConnectionListResponse,
+ )
+
+ def revoke(
+ self,
+ connection_id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ConnectionRevokeResponse:
+ """
+ Revokes Hyperspell's access the given provider and deletes all stored
+ credentials and indexed data.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not connection_id:
+ raise ValueError(f"Expected a non-empty value for `connection_id` but received {connection_id!r}")
+ return self._delete(
+ f"/connections/{connection_id}/revoke",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ConnectionRevokeResponse,
+ )
+
+
+class AsyncConnectionsResource(AsyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> AsyncConnectionsResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/hyperspell/python-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncConnectionsResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncConnectionsResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/hyperspell/python-sdk#with_streaming_response
+ """
+ return AsyncConnectionsResourceWithStreamingResponse(self)
+
+ async def list(
+ self,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ConnectionListResponse:
+ """List all connections for the user."""
+ return await self._get(
+ "/connections/list",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ConnectionListResponse,
+ )
+
+ async def revoke(
+ self,
+ connection_id: str,
+ *,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> ConnectionRevokeResponse:
+ """
+ Revokes Hyperspell's access the given provider and deletes all stored
+ credentials and indexed data.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not connection_id:
+ raise ValueError(f"Expected a non-empty value for `connection_id` but received {connection_id!r}")
+ return await self._delete(
+ f"/connections/{connection_id}/revoke",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ConnectionRevokeResponse,
+ )
+
+
+class ConnectionsResourceWithRawResponse:
+ def __init__(self, connections: ConnectionsResource) -> None:
+ self._connections = connections
+
+ self.list = to_raw_response_wrapper(
+ connections.list,
+ )
+ self.revoke = to_raw_response_wrapper(
+ connections.revoke,
+ )
+
+
+class AsyncConnectionsResourceWithRawResponse:
+ def __init__(self, connections: AsyncConnectionsResource) -> None:
+ self._connections = connections
+
+ self.list = async_to_raw_response_wrapper(
+ connections.list,
+ )
+ self.revoke = async_to_raw_response_wrapper(
+ connections.revoke,
+ )
+
+
+class ConnectionsResourceWithStreamingResponse:
+ def __init__(self, connections: ConnectionsResource) -> None:
+ self._connections = connections
+
+ self.list = to_streamed_response_wrapper(
+ connections.list,
+ )
+ self.revoke = to_streamed_response_wrapper(
+ connections.revoke,
+ )
+
+
+class AsyncConnectionsResourceWithStreamingResponse:
+ def __init__(self, connections: AsyncConnectionsResource) -> None:
+ self._connections = connections
+
+ self.list = async_to_streamed_response_wrapper(
+ connections.list,
+ )
+ self.revoke = async_to_streamed_response_wrapper(
+ connections.revoke,
+ )
diff --git a/src/hyperspell/types/__init__.py b/src/hyperspell/types/__init__.py
index 7764658d..fb573526 100644
--- a/src/hyperspell/types/__init__.py
+++ b/src/hyperspell/types/__init__.py
@@ -16,8 +16,10 @@
from .auth_user_token_params import AuthUserTokenParams as AuthUserTokenParams
from .memory_delete_response import MemoryDeleteResponse as MemoryDeleteResponse
from .memory_status_response import MemoryStatusResponse as MemoryStatusResponse
+from .connection_list_response import ConnectionListResponse as ConnectionListResponse
from .auth_delete_user_response import AuthDeleteUserResponse as AuthDeleteUserResponse
from .integration_list_response import IntegrationListResponse as IntegrationListResponse
+from .connection_revoke_response import ConnectionRevokeResponse as ConnectionRevokeResponse
from .integration_connect_params import IntegrationConnectParams as IntegrationConnectParams
from .evaluate_score_query_params import EvaluateScoreQueryParams as EvaluateScoreQueryParams
from .integration_connect_response import IntegrationConnectResponse as IntegrationConnectResponse
diff --git a/src/hyperspell/types/connection_list_response.py b/src/hyperspell/types/connection_list_response.py
new file mode 100644
index 00000000..95c9ab19
--- /dev/null
+++ b/src/hyperspell/types/connection_list_response.py
@@ -0,0 +1,70 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+from typing_extensions import Literal
+
+from .._models import BaseModel
+
+__all__ = ["ConnectionListResponse", "Connection"]
+
+
+class Connection(BaseModel):
+ id: str
+ """The connection's id"""
+
+ label: Optional[str] = None
+ """The connection's label"""
+
+ provider: Literal[
+ "collections",
+ "vault",
+ "web_crawler",
+ "notion",
+ "slack",
+ "google_calendar",
+ "reddit",
+ "box",
+ "google_drive",
+ "airtable",
+ "algolia",
+ "amplitude",
+ "asana",
+ "ashby",
+ "bamboohr",
+ "basecamp",
+ "bubbles",
+ "calendly",
+ "confluence",
+ "clickup",
+ "datadog",
+ "deel",
+ "discord",
+ "dropbox",
+ "exa",
+ "facebook",
+ "front",
+ "github",
+ "gitlab",
+ "google_docs",
+ "google_mail",
+ "google_sheet",
+ "hubspot",
+ "jira",
+ "linear",
+ "microsoft_teams",
+ "mixpanel",
+ "monday",
+ "outlook",
+ "perplexity",
+ "rippling",
+ "salesforce",
+ "segment",
+ "todoist",
+ "twitter",
+ "zoom",
+ ]
+ """The connection's provider"""
+
+
+class ConnectionListResponse(BaseModel):
+ connections: List[Connection]
diff --git a/src/hyperspell/types/connection_revoke_response.py b/src/hyperspell/types/connection_revoke_response.py
new file mode 100644
index 00000000..63530302
--- /dev/null
+++ b/src/hyperspell/types/connection_revoke_response.py
@@ -0,0 +1,11 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from .._models import BaseModel
+
+__all__ = ["ConnectionRevokeResponse"]
+
+
+class ConnectionRevokeResponse(BaseModel):
+ message: str
+
+ success: bool
diff --git a/tests/api_resources/test_connections.py b/tests/api_resources/test_connections.py
new file mode 100644
index 00000000..d81921ab
--- /dev/null
+++ b/tests/api_resources/test_connections.py
@@ -0,0 +1,150 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from hyperspell import Hyperspell, AsyncHyperspell
+from tests.utils import assert_matches_type
+from hyperspell.types import ConnectionListResponse, ConnectionRevokeResponse
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestConnections:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @parametrize
+ def test_method_list(self, client: Hyperspell) -> None:
+ connection = client.connections.list()
+ assert_matches_type(ConnectionListResponse, connection, path=["response"])
+
+ @parametrize
+ def test_raw_response_list(self, client: Hyperspell) -> None:
+ response = client.connections.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ connection = response.parse()
+ assert_matches_type(ConnectionListResponse, connection, path=["response"])
+
+ @parametrize
+ def test_streaming_response_list(self, client: Hyperspell) -> None:
+ with client.connections.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ connection = response.parse()
+ assert_matches_type(ConnectionListResponse, connection, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_method_revoke(self, client: Hyperspell) -> None:
+ connection = client.connections.revoke(
+ "connection_id",
+ )
+ assert_matches_type(ConnectionRevokeResponse, connection, path=["response"])
+
+ @parametrize
+ def test_raw_response_revoke(self, client: Hyperspell) -> None:
+ response = client.connections.with_raw_response.revoke(
+ "connection_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ connection = response.parse()
+ assert_matches_type(ConnectionRevokeResponse, connection, path=["response"])
+
+ @parametrize
+ def test_streaming_response_revoke(self, client: Hyperspell) -> None:
+ with client.connections.with_streaming_response.revoke(
+ "connection_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ connection = response.parse()
+ assert_matches_type(ConnectionRevokeResponse, connection, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_revoke(self, client: Hyperspell) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `connection_id` but received ''"):
+ client.connections.with_raw_response.revoke(
+ "",
+ )
+
+
+class TestAsyncConnections:
+ 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: AsyncHyperspell) -> None:
+ connection = await async_client.connections.list()
+ assert_matches_type(ConnectionListResponse, connection, path=["response"])
+
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncHyperspell) -> None:
+ response = await async_client.connections.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ connection = await response.parse()
+ assert_matches_type(ConnectionListResponse, connection, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncHyperspell) -> None:
+ async with async_client.connections.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ connection = await response.parse()
+ assert_matches_type(ConnectionListResponse, connection, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_method_revoke(self, async_client: AsyncHyperspell) -> None:
+ connection = await async_client.connections.revoke(
+ "connection_id",
+ )
+ assert_matches_type(ConnectionRevokeResponse, connection, path=["response"])
+
+ @parametrize
+ async def test_raw_response_revoke(self, async_client: AsyncHyperspell) -> None:
+ response = await async_client.connections.with_raw_response.revoke(
+ "connection_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ connection = await response.parse()
+ assert_matches_type(ConnectionRevokeResponse, connection, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_revoke(self, async_client: AsyncHyperspell) -> None:
+ async with async_client.connections.with_streaming_response.revoke(
+ "connection_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ connection = await response.parse()
+ assert_matches_type(ConnectionRevokeResponse, connection, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_revoke(self, async_client: AsyncHyperspell) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `connection_id` but received ''"):
+ await async_client.connections.with_raw_response.revoke(
+ "",
+ )
From 802a6ee6c0b67bddd725727078fc9c3ce2650b69 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 18 Oct 2025 02:26:55 +0000
Subject: [PATCH 06/28] chore: bump `httpx-aiohttp` version to 0.1.9
---
pyproject.toml | 2 +-
requirements-dev.lock | 2 +-
requirements.lock | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 1889a095..ddc8761f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -39,7 +39,7 @@ Homepage = "https://github.com/hyperspell/python-sdk"
Repository = "https://github.com/hyperspell/python-sdk"
[project.optional-dependencies]
-aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.8"]
+aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"]
[tool.rye]
managed = true
diff --git a/requirements-dev.lock b/requirements-dev.lock
index c3a47ff0..40bddc70 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -56,7 +56,7 @@ httpx==0.28.1
# via httpx-aiohttp
# via hyperspell
# via respx
-httpx-aiohttp==0.1.8
+httpx-aiohttp==0.1.9
# via hyperspell
idna==3.4
# via anyio
diff --git a/requirements.lock b/requirements.lock
index e90ee670..0a0fdef1 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -43,7 +43,7 @@ httpcore==1.0.9
httpx==0.28.1
# via httpx-aiohttp
# via hyperspell
-httpx-aiohttp==0.1.8
+httpx-aiohttp==0.1.9
# via hyperspell
idna==3.4
# via anyio
From add1d76538a307e7a1c3fc592207d50351c4c5ca Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sun, 19 Oct 2025 02:32:03 +0000
Subject: [PATCH 07/28] feat(api): api update
---
.stats.yml | 4 ++--
src/hyperspell/types/memory_search_params.py | 2 +-
tests/api_resources/test_memories.py | 4 ++--
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index faae7394..4e409956 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 21
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-f6ce5154226fb08b6df06b813c46950bf10835af91a7e7b64dd54fc077e35dcc.yml
-openapi_spec_hash: f63ed123d263784f1e9971061494f730
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-4f19d2194ceebd4e2f52fefc09e7936f3ce417a943a0709623c5de1edc9bfc78.yml
+openapi_spec_hash: 536a71de5eb401a09790bec0d26a32a0
config_hash: ca45358b5407440488ec988e3ee21412
diff --git a/src/hyperspell/types/memory_search_params.py b/src/hyperspell/types/memory_search_params.py
index 07e692a2..0e1dd1ca 100644
--- a/src/hyperspell/types/memory_search_params.py
+++ b/src/hyperspell/types/memory_search_params.py
@@ -279,7 +279,7 @@ class OptionsWebCrawler(TypedDict, total=False):
max_depth: int
"""Maximum depth to crawl from the starting URL"""
- url: Union[str, object]
+ url: Optional[str]
"""The URL to crawl"""
weight: float
diff --git a/tests/api_resources/test_memories.py b/tests/api_resources/test_memories.py
index 182601a5..c01656d5 100644
--- a/tests/api_resources/test_memories.py
+++ b/tests/api_resources/test_memories.py
@@ -259,7 +259,7 @@ def test_method_search_with_all_params(self, client: Hyperspell) -> None:
"after": parse_datetime("2019-12-27T18:11:19.117Z"),
"before": parse_datetime("2019-12-27T18:11:19.117Z"),
"max_depth": 0,
- "url": "string",
+ "url": "url",
"weight": 0,
},
},
@@ -595,7 +595,7 @@ async def test_method_search_with_all_params(self, async_client: AsyncHyperspell
"after": parse_datetime("2019-12-27T18:11:19.117Z"),
"before": parse_datetime("2019-12-27T18:11:19.117Z"),
"max_depth": 0,
- "url": "string",
+ "url": "url",
"weight": 0,
},
},
From 0f2950160b0842b7522f9a7ac3100b003f276cc4 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 30 Oct 2025 00:32:05 +0000
Subject: [PATCH 08/28] feat(api): api update
---
.stats.yml | 4 +-
api.md | 12 +-
src/hyperspell/resources/memories.py | 23 ++--
src/hyperspell/types/__init__.py | 3 +-
.../{memory.py => memory_get_response.py} | 4 +-
src/hyperspell/types/memory_list_response.py | 105 ++++++++++++++++++
src/hyperspell/types/shared/query_result.py | 105 +++++++++++++++++-
tests/api_resources/test_memories.py | 31 +++---
8 files changed, 249 insertions(+), 38 deletions(-)
rename src/hyperspell/types/{memory.py => memory_get_response.py} (96%)
create mode 100644 src/hyperspell/types/memory_list_response.py
diff --git a/.stats.yml b/.stats.yml
index 4e409956..bb2dbeb0 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 21
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-4f19d2194ceebd4e2f52fefc09e7936f3ce417a943a0709623c5de1edc9bfc78.yml
-openapi_spec_hash: 536a71de5eb401a09790bec0d26a32a0
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-a08047fff67bf0bb9fd8223baba5091eea8477e1933954222e0a690ddce41d5c.yml
+openapi_spec_hash: 7d2a75a868b25e22515a73835d7e13da
config_hash: ca45358b5407440488ec988e3ee21412
diff --git a/api.md b/api.md
index 0452defa..edab7bca 100644
--- a/api.md
+++ b/api.md
@@ -65,15 +65,21 @@ Methods:
Types:
```python
-from hyperspell.types import Memory, MemoryStatus, MemoryDeleteResponse, MemoryStatusResponse
+from hyperspell.types import (
+ MemoryStatus,
+ MemoryListResponse,
+ MemoryDeleteResponse,
+ MemoryGetResponse,
+ MemoryStatusResponse,
+)
```
Methods:
-- client.memories.list(\*\*params) -> SyncCursorPage[Memory]
+- client.memories.list(\*\*params) -> SyncCursorPage[MemoryListResponse]
- client.memories.delete(resource_id, \*, source) -> MemoryDeleteResponse
- client.memories.add(\*\*params) -> MemoryStatus
-- client.memories.get(resource_id, \*, source) -> Memory
+- client.memories.get(resource_id, \*, source) -> MemoryGetResponse
- client.memories.search(\*\*params) -> QueryResult
- client.memories.status() -> MemoryStatusResponse
- client.memories.upload(\*\*params) -> MemoryStatus
diff --git a/src/hyperspell/resources/memories.py b/src/hyperspell/resources/memories.py
index 044081ad..30820969 100644
--- a/src/hyperspell/resources/memories.py
+++ b/src/hyperspell/resources/memories.py
@@ -21,9 +21,10 @@
)
from ..pagination import SyncCursorPage, AsyncCursorPage
from .._base_client import AsyncPaginator, make_request_options
-from ..types.memory import Memory
from ..types.memory_status import MemoryStatus
+from ..types.memory_get_response import MemoryGetResponse
from ..types.shared.query_result import QueryResult
+from ..types.memory_list_response import MemoryListResponse
from ..types.memory_delete_response import MemoryDeleteResponse
from ..types.memory_status_response import MemoryStatusResponse
@@ -113,7 +114,7 @@ def list(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> SyncCursorPage[Memory]:
+ ) -> SyncCursorPage[MemoryListResponse]:
"""This endpoint allows you to paginate through all documents in the index.
You can
@@ -134,7 +135,7 @@ def list(
"""
return self._get_api_list(
"/memories/list",
- page=SyncCursorPage[Memory],
+ page=SyncCursorPage[MemoryListResponse],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -150,7 +151,7 @@ def list(
memory_list_params.MemoryListParams,
),
),
- model=Memory,
+ model=MemoryListResponse,
)
def delete(
@@ -370,7 +371,7 @@ def get(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> Memory:
+ ) -> MemoryGetResponse:
"""
Retrieves a document by provider and resource_id.
@@ -392,7 +393,7 @@ def get(
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=Memory,
+ cast_to=MemoryGetResponse,
)
def search(
@@ -659,7 +660,7 @@ def list(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> AsyncPaginator[Memory, AsyncCursorPage[Memory]]:
+ ) -> AsyncPaginator[MemoryListResponse, AsyncCursorPage[MemoryListResponse]]:
"""This endpoint allows you to paginate through all documents in the index.
You can
@@ -680,7 +681,7 @@ def list(
"""
return self._get_api_list(
"/memories/list",
- page=AsyncCursorPage[Memory],
+ page=AsyncCursorPage[MemoryListResponse],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -696,7 +697,7 @@ def list(
memory_list_params.MemoryListParams,
),
),
- model=Memory,
+ model=MemoryListResponse,
)
async def delete(
@@ -916,7 +917,7 @@ async def get(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> Memory:
+ ) -> MemoryGetResponse:
"""
Retrieves a document by provider and resource_id.
@@ -938,7 +939,7 @@ async def get(
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=Memory,
+ cast_to=MemoryGetResponse,
)
async def search(
diff --git a/src/hyperspell/types/__init__.py b/src/hyperspell/types/__init__.py
index fb573526..2ba5f9b2 100644
--- a/src/hyperspell/types/__init__.py
+++ b/src/hyperspell/types/__init__.py
@@ -3,14 +3,15 @@
from __future__ import annotations
from .token import Token as Token
-from .memory import Memory as Memory
from .shared import QueryResult as QueryResult
from .memory_status import MemoryStatus as MemoryStatus
from .auth_me_response import AuthMeResponse as AuthMeResponse
from .memory_add_params import MemoryAddParams as MemoryAddParams
from .vault_list_params import VaultListParams as VaultListParams
from .memory_list_params import MemoryListParams as MemoryListParams
+from .memory_get_response import MemoryGetResponse as MemoryGetResponse
from .vault_list_response import VaultListResponse as VaultListResponse
+from .memory_list_response import MemoryListResponse as MemoryListResponse
from .memory_search_params import MemorySearchParams as MemorySearchParams
from .memory_upload_params import MemoryUploadParams as MemoryUploadParams
from .auth_user_token_params import AuthUserTokenParams as AuthUserTokenParams
diff --git a/src/hyperspell/types/memory.py b/src/hyperspell/types/memory_get_response.py
similarity index 96%
rename from src/hyperspell/types/memory.py
rename to src/hyperspell/types/memory_get_response.py
index df0de529..093ce1b9 100644
--- a/src/hyperspell/types/memory.py
+++ b/src/hyperspell/types/memory_get_response.py
@@ -8,7 +8,7 @@
from .._models import BaseModel
-__all__ = ["Memory", "Metadata", "MetadataEvent"]
+__all__ = ["MemoryGetResponse", "Metadata", "MetadataEvent"]
class MetadataEvent(BaseModel):
@@ -45,7 +45,7 @@ def __getattr__(self, attr: str) -> object: ...
__pydantic_extra__: Dict[str, object]
-class Memory(BaseModel):
+class MemoryGetResponse(BaseModel):
resource_id: str
source: Literal[
diff --git a/src/hyperspell/types/memory_list_response.py b/src/hyperspell/types/memory_list_response.py
new file mode 100644
index 00000000..692ca8e5
--- /dev/null
+++ b/src/hyperspell/types/memory_list_response.py
@@ -0,0 +1,105 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import TYPE_CHECKING, Dict, List, Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
+
+from .._models import BaseModel
+
+__all__ = ["MemoryListResponse", "Metadata", "MetadataEvent"]
+
+
+class MetadataEvent(BaseModel):
+ message: str
+
+ type: Literal["error", "warning", "info", "success"]
+
+ time: Optional[datetime] = None
+
+
+class Metadata(BaseModel):
+ created_at: Optional[datetime] = None
+
+ events: Optional[List[MetadataEvent]] = None
+
+ indexed_at: Optional[datetime] = None
+
+ last_modified: Optional[datetime] = None
+
+ status: Optional[Literal["pending", "processing", "completed", "failed"]] = None
+
+ url: Optional[str] = None
+
+ if TYPE_CHECKING:
+ # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a
+ # value to this field, so for compatibility we avoid doing it at runtime.
+ __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride]
+
+ # Stub to indicate that arbitrary properties are accepted.
+ # To access properties that are not valid identifiers you can use `getattr`, e.g.
+ # `getattr(obj, '$type')`
+ def __getattr__(self, attr: str) -> object: ...
+ else:
+ __pydantic_extra__: Dict[str, object]
+
+
+class MemoryListResponse(BaseModel):
+ resource_id: str
+
+ source: Literal[
+ "collections",
+ "vault",
+ "web_crawler",
+ "notion",
+ "slack",
+ "google_calendar",
+ "reddit",
+ "box",
+ "google_drive",
+ "airtable",
+ "algolia",
+ "amplitude",
+ "asana",
+ "ashby",
+ "bamboohr",
+ "basecamp",
+ "bubbles",
+ "calendly",
+ "confluence",
+ "clickup",
+ "datadog",
+ "deel",
+ "discord",
+ "dropbox",
+ "exa",
+ "facebook",
+ "front",
+ "github",
+ "gitlab",
+ "google_docs",
+ "google_mail",
+ "google_sheet",
+ "hubspot",
+ "jira",
+ "linear",
+ "microsoft_teams",
+ "mixpanel",
+ "monday",
+ "outlook",
+ "perplexity",
+ "rippling",
+ "salesforce",
+ "segment",
+ "todoist",
+ "twitter",
+ "zoom",
+ ]
+
+ metadata: Optional[Metadata] = None
+
+ score: Optional[float] = None
+ """The relevance of the resource to the query"""
+
+ title: Optional[str] = None
diff --git a/src/hyperspell/types/shared/query_result.py b/src/hyperspell/types/shared/query_result.py
index 67fdb09c..b2c02f9c 100644
--- a/src/hyperspell/types/shared/query_result.py
+++ b/src/hyperspell/types/shared/query_result.py
@@ -1,15 +1,112 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import Dict, List, Optional
+from typing import TYPE_CHECKING, Dict, List, Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
-from ..memory import Memory
from ..._models import BaseModel
-__all__ = ["QueryResult"]
+__all__ = ["QueryResult", "Document", "DocumentMetadata", "DocumentMetadataEvent"]
+
+
+class DocumentMetadataEvent(BaseModel):
+ message: str
+
+ type: Literal["error", "warning", "info", "success"]
+
+ time: Optional[datetime] = None
+
+
+class DocumentMetadata(BaseModel):
+ created_at: Optional[datetime] = None
+
+ events: Optional[List[DocumentMetadataEvent]] = None
+
+ indexed_at: Optional[datetime] = None
+
+ last_modified: Optional[datetime] = None
+
+ status: Optional[Literal["pending", "processing", "completed", "failed"]] = None
+
+ url: Optional[str] = None
+
+ if TYPE_CHECKING:
+ # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a
+ # value to this field, so for compatibility we avoid doing it at runtime.
+ __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride]
+
+ # Stub to indicate that arbitrary properties are accepted.
+ # To access properties that are not valid identifiers you can use `getattr`, e.g.
+ # `getattr(obj, '$type')`
+ def __getattr__(self, attr: str) -> object: ...
+ else:
+ __pydantic_extra__: Dict[str, object]
+
+
+class Document(BaseModel):
+ resource_id: str
+
+ source: Literal[
+ "collections",
+ "vault",
+ "web_crawler",
+ "notion",
+ "slack",
+ "google_calendar",
+ "reddit",
+ "box",
+ "google_drive",
+ "airtable",
+ "algolia",
+ "amplitude",
+ "asana",
+ "ashby",
+ "bamboohr",
+ "basecamp",
+ "bubbles",
+ "calendly",
+ "confluence",
+ "clickup",
+ "datadog",
+ "deel",
+ "discord",
+ "dropbox",
+ "exa",
+ "facebook",
+ "front",
+ "github",
+ "gitlab",
+ "google_docs",
+ "google_mail",
+ "google_sheet",
+ "hubspot",
+ "jira",
+ "linear",
+ "microsoft_teams",
+ "mixpanel",
+ "monday",
+ "outlook",
+ "perplexity",
+ "rippling",
+ "salesforce",
+ "segment",
+ "todoist",
+ "twitter",
+ "zoom",
+ ]
+
+ metadata: Optional[DocumentMetadata] = None
+
+ score: Optional[float] = None
+ """The relevance of the resource to the query"""
+
+ title: Optional[str] = None
class QueryResult(BaseModel):
- documents: List[Memory]
+ documents: List[Document]
answer: Optional[str] = None
"""The answer to the query, if the request was set to answer."""
diff --git a/tests/api_resources/test_memories.py b/tests/api_resources/test_memories.py
index c01656d5..e65db28c 100644
--- a/tests/api_resources/test_memories.py
+++ b/tests/api_resources/test_memories.py
@@ -10,8 +10,9 @@
from hyperspell import Hyperspell, AsyncHyperspell
from tests.utils import assert_matches_type
from hyperspell.types import (
- Memory,
MemoryStatus,
+ MemoryGetResponse,
+ MemoryListResponse,
MemoryDeleteResponse,
MemoryStatusResponse,
)
@@ -28,7 +29,7 @@ class TestMemories:
@parametrize
def test_method_list(self, client: Hyperspell) -> None:
memory = client.memories.list()
- assert_matches_type(SyncCursorPage[Memory], memory, path=["response"])
+ assert_matches_type(SyncCursorPage[MemoryListResponse], memory, path=["response"])
@parametrize
def test_method_list_with_all_params(self, client: Hyperspell) -> None:
@@ -38,7 +39,7 @@ def test_method_list_with_all_params(self, client: Hyperspell) -> None:
size=0,
source="collections",
)
- assert_matches_type(SyncCursorPage[Memory], memory, path=["response"])
+ assert_matches_type(SyncCursorPage[MemoryListResponse], memory, path=["response"])
@parametrize
def test_raw_response_list(self, client: Hyperspell) -> None:
@@ -47,7 +48,7 @@ def test_raw_response_list(self, client: Hyperspell) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = response.parse()
- assert_matches_type(SyncCursorPage[Memory], memory, path=["response"])
+ assert_matches_type(SyncCursorPage[MemoryListResponse], memory, path=["response"])
@parametrize
def test_streaming_response_list(self, client: Hyperspell) -> None:
@@ -56,7 +57,7 @@ def test_streaming_response_list(self, client: Hyperspell) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = response.parse()
- assert_matches_type(SyncCursorPage[Memory], memory, path=["response"])
+ assert_matches_type(SyncCursorPage[MemoryListResponse], memory, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -150,7 +151,7 @@ def test_method_get(self, client: Hyperspell) -> None:
resource_id="resource_id",
source="collections",
)
- assert_matches_type(Memory, memory, path=["response"])
+ assert_matches_type(MemoryGetResponse, memory, path=["response"])
@parametrize
def test_raw_response_get(self, client: Hyperspell) -> None:
@@ -162,7 +163,7 @@ def test_raw_response_get(self, client: Hyperspell) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = response.parse()
- assert_matches_type(Memory, memory, path=["response"])
+ assert_matches_type(MemoryGetResponse, memory, path=["response"])
@parametrize
def test_streaming_response_get(self, client: Hyperspell) -> None:
@@ -174,7 +175,7 @@ def test_streaming_response_get(self, client: Hyperspell) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = response.parse()
- assert_matches_type(Memory, memory, path=["response"])
+ assert_matches_type(MemoryGetResponse, memory, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -364,7 +365,7 @@ class TestAsyncMemories:
@parametrize
async def test_method_list(self, async_client: AsyncHyperspell) -> None:
memory = await async_client.memories.list()
- assert_matches_type(AsyncCursorPage[Memory], memory, path=["response"])
+ assert_matches_type(AsyncCursorPage[MemoryListResponse], memory, path=["response"])
@parametrize
async def test_method_list_with_all_params(self, async_client: AsyncHyperspell) -> None:
@@ -374,7 +375,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncHyperspell)
size=0,
source="collections",
)
- assert_matches_type(AsyncCursorPage[Memory], memory, path=["response"])
+ assert_matches_type(AsyncCursorPage[MemoryListResponse], memory, path=["response"])
@parametrize
async def test_raw_response_list(self, async_client: AsyncHyperspell) -> None:
@@ -383,7 +384,7 @@ async def test_raw_response_list(self, async_client: AsyncHyperspell) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = await response.parse()
- assert_matches_type(AsyncCursorPage[Memory], memory, path=["response"])
+ assert_matches_type(AsyncCursorPage[MemoryListResponse], memory, path=["response"])
@parametrize
async def test_streaming_response_list(self, async_client: AsyncHyperspell) -> None:
@@ -392,7 +393,7 @@ async def test_streaming_response_list(self, async_client: AsyncHyperspell) -> N
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = await response.parse()
- assert_matches_type(AsyncCursorPage[Memory], memory, path=["response"])
+ assert_matches_type(AsyncCursorPage[MemoryListResponse], memory, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -486,7 +487,7 @@ async def test_method_get(self, async_client: AsyncHyperspell) -> None:
resource_id="resource_id",
source="collections",
)
- assert_matches_type(Memory, memory, path=["response"])
+ assert_matches_type(MemoryGetResponse, memory, path=["response"])
@parametrize
async def test_raw_response_get(self, async_client: AsyncHyperspell) -> None:
@@ -498,7 +499,7 @@ async def test_raw_response_get(self, async_client: AsyncHyperspell) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = await response.parse()
- assert_matches_type(Memory, memory, path=["response"])
+ assert_matches_type(MemoryGetResponse, memory, path=["response"])
@parametrize
async def test_streaming_response_get(self, async_client: AsyncHyperspell) -> None:
@@ -510,7 +511,7 @@ async def test_streaming_response_get(self, async_client: AsyncHyperspell) -> No
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = await response.parse()
- assert_matches_type(Memory, memory, path=["response"])
+ assert_matches_type(MemoryGetResponse, memory, path=["response"])
assert cast(Any, response.is_closed) is True
From 669e4cf750158df6aa381083911c051a6afb8f07 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 30 Oct 2025 02:58:48 +0000
Subject: [PATCH 09/28] fix(client): close streams without requiring full
consumption
---
src/hyperspell/_streaming.py | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/src/hyperspell/_streaming.py b/src/hyperspell/_streaming.py
index 327dec92..c3eb77e6 100644
--- a/src/hyperspell/_streaming.py
+++ b/src/hyperspell/_streaming.py
@@ -57,9 +57,8 @@ def __stream__(self) -> Iterator[_T]:
for sse in iterator:
yield process_data(data=sse.json(), cast_to=cast_to, response=response)
- # Ensure the entire stream is consumed
- for _sse in iterator:
- ...
+ # As we might not fully consume the response stream, we need to close it explicitly
+ response.close()
def __enter__(self) -> Self:
return self
@@ -121,9 +120,8 @@ async def __stream__(self) -> AsyncIterator[_T]:
async for sse in iterator:
yield process_data(data=sse.json(), cast_to=cast_to, response=response)
- # Ensure the entire stream is consumed
- async for _sse in iterator:
- ...
+ # As we might not fully consume the response stream, we need to close it explicitly
+ await response.aclose()
async def __aenter__(self) -> Self:
return self
From 01d946a29b5dbfd37f2f7a7c93f6824c5865af1b Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 31 Oct 2025 04:24:57 +0000
Subject: [PATCH 10/28] chore(internal/tests): avoid race condition with
implicit client cleanup
---
tests/test_client.py | 380 ++++++++++++++++++++++++-------------------
1 file changed, 212 insertions(+), 168 deletions(-)
diff --git a/tests/test_client.py b/tests/test_client.py
index eff94130..21615814 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -60,55 +60,53 @@ def _get_open_connections(client: Hyperspell | AsyncHyperspell) -> int:
class TestHyperspell:
- client = Hyperspell(base_url=base_url, api_key=api_key, user_id=user_id, _strict_response_validation=True)
-
@pytest.mark.respx(base_url=base_url)
- def test_raw_response(self, respx_mock: MockRouter) -> None:
+ def test_raw_response(self, respx_mock: MockRouter, client: Hyperspell) -> None:
respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
- response = self.client.post("/foo", cast_to=httpx.Response)
+ response = client.post("/foo", cast_to=httpx.Response)
assert response.status_code == 200
assert isinstance(response, httpx.Response)
assert response.json() == {"foo": "bar"}
@pytest.mark.respx(base_url=base_url)
- def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None:
+ def test_raw_response_for_binary(self, respx_mock: MockRouter, client: Hyperspell) -> None:
respx_mock.post("/foo").mock(
return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}')
)
- response = self.client.post("/foo", cast_to=httpx.Response)
+ response = client.post("/foo", cast_to=httpx.Response)
assert response.status_code == 200
assert isinstance(response, httpx.Response)
assert response.json() == {"foo": "bar"}
- def test_copy(self) -> None:
- copied = self.client.copy()
- assert id(copied) != id(self.client)
+ def test_copy(self, client: Hyperspell) -> None:
+ copied = client.copy()
+ assert id(copied) != id(client)
- copied = self.client.copy(api_key="another My API Key")
+ copied = client.copy(api_key="another My API Key")
assert copied.api_key == "another My API Key"
- assert self.client.api_key == "My API Key"
+ assert client.api_key == "My API Key"
- copied = self.client.copy(user_id="another My User ID")
+ copied = client.copy(user_id="another My User ID")
assert copied.user_id == "another My User ID"
- assert self.client.user_id == "My User ID"
+ assert client.user_id == "My User ID"
- def test_copy_default_options(self) -> None:
+ def test_copy_default_options(self, client: Hyperspell) -> None:
# options that have a default are overridden correctly
- copied = self.client.copy(max_retries=7)
+ copied = client.copy(max_retries=7)
assert copied.max_retries == 7
- assert self.client.max_retries == 2
+ assert client.max_retries == 2
copied2 = copied.copy(max_retries=6)
assert copied2.max_retries == 6
assert copied.max_retries == 7
# timeout
- assert isinstance(self.client.timeout, httpx.Timeout)
- copied = self.client.copy(timeout=None)
+ assert isinstance(client.timeout, httpx.Timeout)
+ copied = client.copy(timeout=None)
assert copied.timeout is None
- assert isinstance(self.client.timeout, httpx.Timeout)
+ assert isinstance(client.timeout, httpx.Timeout)
def test_copy_default_headers(self) -> None:
client = Hyperspell(
@@ -147,6 +145,7 @@ def test_copy_default_headers(self) -> None:
match="`default_headers` and `set_default_headers` arguments are mutually exclusive",
):
client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"})
+ client.close()
def test_copy_default_query(self) -> None:
client = Hyperspell(
@@ -188,13 +187,15 @@ def test_copy_default_query(self) -> None:
):
client.copy(set_default_query={}, default_query={"foo": "Bar"})
- def test_copy_signature(self) -> None:
+ client.close()
+
+ def test_copy_signature(self, client: Hyperspell) -> None:
# ensure the same parameters that can be passed to the client are defined in the `.copy()` method
init_signature = inspect.signature(
# mypy doesn't like that we access the `__init__` property.
- self.client.__init__, # type: ignore[misc]
+ client.__init__, # type: ignore[misc]
)
- copy_signature = inspect.signature(self.client.copy)
+ copy_signature = inspect.signature(client.copy)
exclude_params = {"transport", "proxies", "_strict_response_validation"}
for name in init_signature.parameters.keys():
@@ -205,12 +206,12 @@ def test_copy_signature(self) -> None:
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:
+ def test_copy_build_request(self, client: Hyperspell) -> None:
options = FinalRequestOptions(method="get", url="/foo")
def build_request(options: FinalRequestOptions) -> None:
- client = self.client.copy()
- client._build_request(options)
+ client_copy = client.copy()
+ client_copy._build_request(options)
# ensure that the machinery is warmed up before tracing starts.
build_request(options)
@@ -267,14 +268,12 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic
print(frame)
raise AssertionError()
- def test_request_timeout(self) -> None:
- request = self.client._build_request(FinalRequestOptions(method="get", url="/foo"))
+ def test_request_timeout(self, client: Hyperspell) -> None:
+ request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == DEFAULT_TIMEOUT
- request = self.client._build_request(
- FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))
- )
+ request = client._build_request(FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)))
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == httpx.Timeout(100.0)
@@ -291,6 +290,8 @@ def test_client_timeout_option(self) -> None:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == httpx.Timeout(0)
+ client.close()
+
def test_http_client_timeout_option(self) -> None:
# custom timeout given to the httpx client should be used
with httpx.Client(timeout=None) as http_client:
@@ -306,6 +307,8 @@ def test_http_client_timeout_option(self) -> None:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == httpx.Timeout(None)
+ client.close()
+
# no timeout given to the httpx client should not use the httpx default
with httpx.Client() as http_client:
client = Hyperspell(
@@ -320,6 +323,8 @@ def test_http_client_timeout_option(self) -> None:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == DEFAULT_TIMEOUT
+ client.close()
+
# explicitly passing the default timeout currently results in it being ignored
with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
client = Hyperspell(
@@ -334,6 +339,8 @@ def test_http_client_timeout_option(self) -> None:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == DEFAULT_TIMEOUT # our default
+ client.close()
+
async def test_invalid_http_client(self) -> None:
with pytest.raises(TypeError, match="Invalid `http_client` arg"):
async with httpx.AsyncClient() as http_client:
@@ -346,18 +353,18 @@ async def test_invalid_http_client(self) -> None:
)
def test_default_headers_option(self) -> None:
- client = Hyperspell(
+ test_client = Hyperspell(
base_url=base_url,
api_key=api_key,
user_id=user_id,
_strict_response_validation=True,
default_headers={"X-Foo": "bar"},
)
- request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
+ request = test_client._build_request(FinalRequestOptions(method="get", url="/foo"))
assert request.headers.get("x-foo") == "bar"
assert request.headers.get("x-stainless-lang") == "python"
- client2 = Hyperspell(
+ test_client2 = Hyperspell(
base_url=base_url,
api_key=api_key,
user_id=user_id,
@@ -367,10 +374,13 @@ def test_default_headers_option(self) -> None:
"X-Stainless-Lang": "my-overriding-header",
},
)
- request = client2._build_request(FinalRequestOptions(method="get", url="/foo"))
+ request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo"))
assert request.headers.get("x-foo") == "stainless"
assert request.headers.get("x-stainless-lang") == "my-overriding-header"
+ test_client.close()
+ test_client2.close()
+
def test_validate_headers(self) -> None:
client = Hyperspell(base_url=base_url, api_key=api_key, user_id=user_id, _strict_response_validation=True)
request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
@@ -405,8 +415,10 @@ def test_default_query_option(self) -> None:
url = httpx.URL(request.url)
assert dict(url.params) == {"foo": "baz", "query_param": "overridden"}
- def test_request_extra_json(self) -> None:
- request = self.client._build_request(
+ client.close()
+
+ def test_request_extra_json(self, client: Hyperspell) -> None:
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -417,7 +429,7 @@ def test_request_extra_json(self) -> None:
data = json.loads(request.content.decode("utf-8"))
assert data == {"foo": "bar", "baz": False}
- request = self.client._build_request(
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -428,7 +440,7 @@ def test_request_extra_json(self) -> None:
assert data == {"baz": False}
# `extra_json` takes priority over `json_data` when keys clash
- request = self.client._build_request(
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -439,8 +451,8 @@ def test_request_extra_json(self) -> None:
data = json.loads(request.content.decode("utf-8"))
assert data == {"foo": "bar", "baz": None}
- def test_request_extra_headers(self) -> None:
- request = self.client._build_request(
+ def test_request_extra_headers(self, client: Hyperspell) -> None:
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -450,7 +462,7 @@ def test_request_extra_headers(self) -> None:
assert request.headers.get("X-Foo") == "Foo"
# `extra_headers` takes priority over `default_headers` when keys clash
- request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request(
+ request = client.with_options(default_headers={"X-Bar": "true"})._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -461,8 +473,8 @@ def test_request_extra_headers(self) -> None:
)
assert request.headers.get("X-Bar") == "false"
- def test_request_extra_query(self) -> None:
- request = self.client._build_request(
+ def test_request_extra_query(self, client: Hyperspell) -> None:
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -475,7 +487,7 @@ def test_request_extra_query(self) -> None:
assert params == {"my_query_param": "Foo"}
# if both `query` and `extra_query` are given, they are merged
- request = self.client._build_request(
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -489,7 +501,7 @@ def test_request_extra_query(self) -> None:
assert params == {"bar": "1", "foo": "2"}
# `extra_query` takes priority over `query` when keys clash
- request = self.client._build_request(
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -532,7 +544,7 @@ def test_multipart_repeating_array(self, client: Hyperspell) -> None:
]
@pytest.mark.respx(base_url=base_url)
- def test_basic_union_response(self, respx_mock: MockRouter) -> None:
+ def test_basic_union_response(self, respx_mock: MockRouter, client: Hyperspell) -> None:
class Model1(BaseModel):
name: str
@@ -541,12 +553,12 @@ class Model2(BaseModel):
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
- response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
+ response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
assert isinstance(response, Model2)
assert response.foo == "bar"
@pytest.mark.respx(base_url=base_url)
- def test_union_response_different_types(self, respx_mock: MockRouter) -> None:
+ def test_union_response_different_types(self, respx_mock: MockRouter, client: Hyperspell) -> None:
"""Union of objects with the same field name using a different type"""
class Model1(BaseModel):
@@ -557,18 +569,18 @@ class Model2(BaseModel):
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
- response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
+ response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
assert isinstance(response, Model2)
assert response.foo == "bar"
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1}))
- response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
+ response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
assert isinstance(response, Model1)
assert response.foo == 1
@pytest.mark.respx(base_url=base_url)
- def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None:
+ def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter, client: Hyperspell) -> None:
"""
Response that sets Content-Type to something other than application/json but returns json data
"""
@@ -584,7 +596,7 @@ class Model(BaseModel):
)
)
- response = self.client.get("/foo", cast_to=Model)
+ response = client.get("/foo", cast_to=Model)
assert isinstance(response, Model)
assert response.foo == 2
@@ -598,6 +610,8 @@ def test_base_url_setter(self) -> None:
assert client.base_url == "https://example.com/from_setter/"
+ client.close()
+
def test_base_url_env(self) -> None:
with update_env(HYPERSPELL_BASE_URL="http://localhost:5000/from/env"):
client = Hyperspell(api_key=api_key, user_id=user_id, _strict_response_validation=True)
@@ -631,6 +645,7 @@ def test_base_url_trailing_slash(self, client: Hyperspell) -> None:
),
)
assert request.url == "http://localhost:5000/custom/path/foo"
+ client.close()
@pytest.mark.parametrize(
"client",
@@ -660,6 +675,7 @@ def test_base_url_no_trailing_slash(self, client: Hyperspell) -> None:
),
)
assert request.url == "http://localhost:5000/custom/path/foo"
+ client.close()
@pytest.mark.parametrize(
"client",
@@ -689,35 +705,36 @@ def test_absolute_request_url(self, client: Hyperspell) -> None:
),
)
assert request.url == "https://myapi.com/foo"
+ client.close()
def test_copied_client_does_not_close_http(self) -> None:
- client = Hyperspell(base_url=base_url, api_key=api_key, user_id=user_id, _strict_response_validation=True)
- assert not client.is_closed()
+ test_client = Hyperspell(base_url=base_url, api_key=api_key, user_id=user_id, _strict_response_validation=True)
+ assert not test_client.is_closed()
- copied = client.copy()
- assert copied is not client
+ copied = test_client.copy()
+ assert copied is not test_client
del copied
- assert not client.is_closed()
+ assert not test_client.is_closed()
def test_client_context_manager(self) -> None:
- client = Hyperspell(base_url=base_url, api_key=api_key, user_id=user_id, _strict_response_validation=True)
- with client as c2:
- assert c2 is client
+ test_client = Hyperspell(base_url=base_url, api_key=api_key, user_id=user_id, _strict_response_validation=True)
+ with test_client as c2:
+ assert c2 is test_client
assert not c2.is_closed()
- assert not client.is_closed()
- assert client.is_closed()
+ assert not test_client.is_closed()
+ assert test_client.is_closed()
@pytest.mark.respx(base_url=base_url)
- def test_client_response_validation_error(self, respx_mock: MockRouter) -> None:
+ def test_client_response_validation_error(self, respx_mock: MockRouter, client: Hyperspell) -> None:
class Model(BaseModel):
foo: str
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}}))
with pytest.raises(APIResponseValidationError) as exc:
- self.client.get("/foo", cast_to=Model)
+ client.get("/foo", cast_to=Model)
assert isinstance(exc.value.__cause__, ValidationError)
@@ -745,11 +762,16 @@ class Model(BaseModel):
with pytest.raises(APIResponseValidationError):
strict_client.get("/foo", cast_to=Model)
- client = Hyperspell(base_url=base_url, api_key=api_key, user_id=user_id, _strict_response_validation=False)
+ non_strict_client = Hyperspell(
+ base_url=base_url, api_key=api_key, user_id=user_id, _strict_response_validation=False
+ )
- response = client.get("/foo", cast_to=Model)
+ response = non_strict_client.get("/foo", cast_to=Model)
assert isinstance(response, str) # type: ignore[unreachable]
+ strict_client.close()
+ non_strict_client.close()
+
@pytest.mark.parametrize(
"remaining_retries,retry_after,timeout",
[
@@ -772,9 +794,9 @@ class Model(BaseModel):
],
)
@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
- def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
- client = Hyperspell(base_url=base_url, api_key=api_key, user_id=user_id, _strict_response_validation=True)
-
+ def test_parse_retry_after_header(
+ self, remaining_retries: int, retry_after: str, timeout: float, client: Hyperspell
+ ) -> None:
headers = httpx.Headers({"retry-after": retry_after})
options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
@@ -788,7 +810,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien
with pytest.raises(APITimeoutError):
client.memories.with_streaming_response.add(text="text").__enter__()
- assert _get_open_connections(self.client) == 0
+ assert _get_open_connections(client) == 0
@mock.patch("hyperspell._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
@@ -797,7 +819,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client
with pytest.raises(APIStatusError):
client.memories.with_streaming_response.add(text="text").__enter__()
- assert _get_open_connections(self.client) == 0
+ assert _get_open_connections(client) == 0
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
@mock.patch("hyperspell._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@@ -899,87 +921,81 @@ def test_default_client_creation(self) -> None:
)
@pytest.mark.respx(base_url=base_url)
- def test_follow_redirects(self, respx_mock: MockRouter) -> None:
+ def test_follow_redirects(self, respx_mock: MockRouter, client: Hyperspell) -> 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)
+ response = 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:
+ def test_follow_redirects_disabled(self, respx_mock: MockRouter, client: Hyperspell) -> 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
- )
+ 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 TestAsyncHyperspell:
- client = AsyncHyperspell(base_url=base_url, api_key=api_key, user_id=user_id, _strict_response_validation=True)
-
@pytest.mark.respx(base_url=base_url)
- @pytest.mark.asyncio
- async def test_raw_response(self, respx_mock: MockRouter) -> None:
+ async def test_raw_response(self, respx_mock: MockRouter, async_client: AsyncHyperspell) -> None:
respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
- response = await self.client.post("/foo", cast_to=httpx.Response)
+ response = await async_client.post("/foo", cast_to=httpx.Response)
assert response.status_code == 200
assert isinstance(response, httpx.Response)
assert response.json() == {"foo": "bar"}
@pytest.mark.respx(base_url=base_url)
- @pytest.mark.asyncio
- async def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None:
+ async def test_raw_response_for_binary(self, respx_mock: MockRouter, async_client: AsyncHyperspell) -> None:
respx_mock.post("/foo").mock(
return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}')
)
- response = await self.client.post("/foo", cast_to=httpx.Response)
+ response = await async_client.post("/foo", cast_to=httpx.Response)
assert response.status_code == 200
assert isinstance(response, httpx.Response)
assert response.json() == {"foo": "bar"}
- def test_copy(self) -> None:
- copied = self.client.copy()
- assert id(copied) != id(self.client)
+ def test_copy(self, async_client: AsyncHyperspell) -> None:
+ copied = async_client.copy()
+ assert id(copied) != id(async_client)
- copied = self.client.copy(api_key="another My API Key")
+ copied = async_client.copy(api_key="another My API Key")
assert copied.api_key == "another My API Key"
- assert self.client.api_key == "My API Key"
+ assert async_client.api_key == "My API Key"
- copied = self.client.copy(user_id="another My User ID")
+ copied = async_client.copy(user_id="another My User ID")
assert copied.user_id == "another My User ID"
- assert self.client.user_id == "My User ID"
+ assert async_client.user_id == "My User ID"
- def test_copy_default_options(self) -> None:
+ def test_copy_default_options(self, async_client: AsyncHyperspell) -> None:
# options that have a default are overridden correctly
- copied = self.client.copy(max_retries=7)
+ copied = async_client.copy(max_retries=7)
assert copied.max_retries == 7
- assert self.client.max_retries == 2
+ assert async_client.max_retries == 2
copied2 = copied.copy(max_retries=6)
assert copied2.max_retries == 6
assert copied.max_retries == 7
# timeout
- assert isinstance(self.client.timeout, httpx.Timeout)
- copied = self.client.copy(timeout=None)
+ assert isinstance(async_client.timeout, httpx.Timeout)
+ copied = async_client.copy(timeout=None)
assert copied.timeout is None
- assert isinstance(self.client.timeout, httpx.Timeout)
+ assert isinstance(async_client.timeout, httpx.Timeout)
- def test_copy_default_headers(self) -> None:
+ async def test_copy_default_headers(self) -> None:
client = AsyncHyperspell(
base_url=base_url,
api_key=api_key,
@@ -1016,8 +1032,9 @@ def test_copy_default_headers(self) -> None:
match="`default_headers` and `set_default_headers` arguments are mutually exclusive",
):
client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"})
+ await client.close()
- def test_copy_default_query(self) -> None:
+ async def test_copy_default_query(self) -> None:
client = AsyncHyperspell(
base_url=base_url,
api_key=api_key,
@@ -1057,13 +1074,15 @@ def test_copy_default_query(self) -> None:
):
client.copy(set_default_query={}, default_query={"foo": "Bar"})
- def test_copy_signature(self) -> None:
+ await client.close()
+
+ def test_copy_signature(self, async_client: AsyncHyperspell) -> None:
# ensure the same parameters that can be passed to the client are defined in the `.copy()` method
init_signature = inspect.signature(
# mypy doesn't like that we access the `__init__` property.
- self.client.__init__, # type: ignore[misc]
+ async_client.__init__, # type: ignore[misc]
)
- copy_signature = inspect.signature(self.client.copy)
+ copy_signature = inspect.signature(async_client.copy)
exclude_params = {"transport", "proxies", "_strict_response_validation"}
for name in init_signature.parameters.keys():
@@ -1074,12 +1093,12 @@ def test_copy_signature(self) -> None:
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:
+ def test_copy_build_request(self, async_client: AsyncHyperspell) -> None:
options = FinalRequestOptions(method="get", url="/foo")
def build_request(options: FinalRequestOptions) -> None:
- client = self.client.copy()
- client._build_request(options)
+ client_copy = async_client.copy()
+ client_copy._build_request(options)
# ensure that the machinery is warmed up before tracing starts.
build_request(options)
@@ -1136,12 +1155,12 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic
print(frame)
raise AssertionError()
- async def test_request_timeout(self) -> None:
- request = self.client._build_request(FinalRequestOptions(method="get", url="/foo"))
+ async def test_request_timeout(self, async_client: AsyncHyperspell) -> None:
+ request = async_client._build_request(FinalRequestOptions(method="get", url="/foo"))
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == DEFAULT_TIMEOUT
- request = self.client._build_request(
+ request = async_client._build_request(
FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))
)
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
@@ -1160,6 +1179,8 @@ async def test_client_timeout_option(self) -> None:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == httpx.Timeout(0)
+ await client.close()
+
async def test_http_client_timeout_option(self) -> None:
# custom timeout given to the httpx client should be used
async with httpx.AsyncClient(timeout=None) as http_client:
@@ -1175,6 +1196,8 @@ async def test_http_client_timeout_option(self) -> None:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == httpx.Timeout(None)
+ await client.close()
+
# no timeout given to the httpx client should not use the httpx default
async with httpx.AsyncClient() as http_client:
client = AsyncHyperspell(
@@ -1189,6 +1212,8 @@ async def test_http_client_timeout_option(self) -> None:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == DEFAULT_TIMEOUT
+ await client.close()
+
# explicitly passing the default timeout currently results in it being ignored
async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
client = AsyncHyperspell(
@@ -1203,6 +1228,8 @@ async def test_http_client_timeout_option(self) -> None:
timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore
assert timeout == DEFAULT_TIMEOUT # our default
+ await client.close()
+
def test_invalid_http_client(self) -> None:
with pytest.raises(TypeError, match="Invalid `http_client` arg"):
with httpx.Client() as http_client:
@@ -1214,19 +1241,19 @@ def test_invalid_http_client(self) -> None:
http_client=cast(Any, http_client),
)
- def test_default_headers_option(self) -> None:
- client = AsyncHyperspell(
+ async def test_default_headers_option(self) -> None:
+ test_client = AsyncHyperspell(
base_url=base_url,
api_key=api_key,
user_id=user_id,
_strict_response_validation=True,
default_headers={"X-Foo": "bar"},
)
- request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
+ request = test_client._build_request(FinalRequestOptions(method="get", url="/foo"))
assert request.headers.get("x-foo") == "bar"
assert request.headers.get("x-stainless-lang") == "python"
- client2 = AsyncHyperspell(
+ test_client2 = AsyncHyperspell(
base_url=base_url,
api_key=api_key,
user_id=user_id,
@@ -1236,10 +1263,13 @@ def test_default_headers_option(self) -> None:
"X-Stainless-Lang": "my-overriding-header",
},
)
- request = client2._build_request(FinalRequestOptions(method="get", url="/foo"))
+ request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo"))
assert request.headers.get("x-foo") == "stainless"
assert request.headers.get("x-stainless-lang") == "my-overriding-header"
+ await test_client.close()
+ await test_client2.close()
+
def test_validate_headers(self) -> None:
client = AsyncHyperspell(base_url=base_url, api_key=api_key, user_id=user_id, _strict_response_validation=True)
request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
@@ -1254,7 +1284,7 @@ def test_validate_headers(self) -> None:
)
_ = client2
- def test_default_query_option(self) -> None:
+ async def test_default_query_option(self) -> None:
client = AsyncHyperspell(
base_url=base_url,
api_key=api_key,
@@ -1276,8 +1306,10 @@ def test_default_query_option(self) -> None:
url = httpx.URL(request.url)
assert dict(url.params) == {"foo": "baz", "query_param": "overridden"}
- def test_request_extra_json(self) -> None:
- request = self.client._build_request(
+ await client.close()
+
+ def test_request_extra_json(self, client: Hyperspell) -> None:
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1288,7 +1320,7 @@ def test_request_extra_json(self) -> None:
data = json.loads(request.content.decode("utf-8"))
assert data == {"foo": "bar", "baz": False}
- request = self.client._build_request(
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1299,7 +1331,7 @@ def test_request_extra_json(self) -> None:
assert data == {"baz": False}
# `extra_json` takes priority over `json_data` when keys clash
- request = self.client._build_request(
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1310,8 +1342,8 @@ def test_request_extra_json(self) -> None:
data = json.loads(request.content.decode("utf-8"))
assert data == {"foo": "bar", "baz": None}
- def test_request_extra_headers(self) -> None:
- request = self.client._build_request(
+ def test_request_extra_headers(self, client: Hyperspell) -> None:
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1321,7 +1353,7 @@ def test_request_extra_headers(self) -> None:
assert request.headers.get("X-Foo") == "Foo"
# `extra_headers` takes priority over `default_headers` when keys clash
- request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request(
+ request = client.with_options(default_headers={"X-Bar": "true"})._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1332,8 +1364,8 @@ def test_request_extra_headers(self) -> None:
)
assert request.headers.get("X-Bar") == "false"
- def test_request_extra_query(self) -> None:
- request = self.client._build_request(
+ def test_request_extra_query(self, client: Hyperspell) -> None:
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1346,7 +1378,7 @@ def test_request_extra_query(self) -> None:
assert params == {"my_query_param": "Foo"}
# if both `query` and `extra_query` are given, they are merged
- request = self.client._build_request(
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1360,7 +1392,7 @@ def test_request_extra_query(self) -> None:
assert params == {"bar": "1", "foo": "2"}
# `extra_query` takes priority over `query` when keys clash
- request = self.client._build_request(
+ request = client._build_request(
FinalRequestOptions(
method="post",
url="/foo",
@@ -1403,7 +1435,7 @@ def test_multipart_repeating_array(self, async_client: AsyncHyperspell) -> None:
]
@pytest.mark.respx(base_url=base_url)
- async def test_basic_union_response(self, respx_mock: MockRouter) -> None:
+ async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncHyperspell) -> None:
class Model1(BaseModel):
name: str
@@ -1412,12 +1444,12 @@ class Model2(BaseModel):
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
- response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
+ response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
assert isinstance(response, Model2)
assert response.foo == "bar"
@pytest.mark.respx(base_url=base_url)
- async def test_union_response_different_types(self, respx_mock: MockRouter) -> None:
+ async def test_union_response_different_types(self, respx_mock: MockRouter, async_client: AsyncHyperspell) -> None:
"""Union of objects with the same field name using a different type"""
class Model1(BaseModel):
@@ -1428,18 +1460,20 @@ class Model2(BaseModel):
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
- response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
+ response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
assert isinstance(response, Model2)
assert response.foo == "bar"
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1}))
- response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
+ response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2]))
assert isinstance(response, Model1)
assert response.foo == 1
@pytest.mark.respx(base_url=base_url)
- async def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None:
+ async def test_non_application_json_content_type_for_json_data(
+ self, respx_mock: MockRouter, async_client: AsyncHyperspell
+ ) -> None:
"""
Response that sets Content-Type to something other than application/json but returns json data
"""
@@ -1455,11 +1489,11 @@ class Model(BaseModel):
)
)
- response = await self.client.get("/foo", cast_to=Model)
+ response = await async_client.get("/foo", cast_to=Model)
assert isinstance(response, Model)
assert response.foo == 2
- def test_base_url_setter(self) -> None:
+ async def test_base_url_setter(self) -> None:
client = AsyncHyperspell(
base_url="https://example.com/from_init", api_key=api_key, user_id=user_id, _strict_response_validation=True
)
@@ -1469,7 +1503,9 @@ def test_base_url_setter(self) -> None:
assert client.base_url == "https://example.com/from_setter/"
- def test_base_url_env(self) -> None:
+ await client.close()
+
+ async def test_base_url_env(self) -> None:
with update_env(HYPERSPELL_BASE_URL="http://localhost:5000/from/env"):
client = AsyncHyperspell(api_key=api_key, user_id=user_id, _strict_response_validation=True)
assert client.base_url == "http://localhost:5000/from/env/"
@@ -1493,7 +1529,7 @@ def test_base_url_env(self) -> None:
],
ids=["standard", "custom http client"],
)
- def test_base_url_trailing_slash(self, client: AsyncHyperspell) -> None:
+ async def test_base_url_trailing_slash(self, client: AsyncHyperspell) -> None:
request = client._build_request(
FinalRequestOptions(
method="post",
@@ -1502,6 +1538,7 @@ def test_base_url_trailing_slash(self, client: AsyncHyperspell) -> None:
),
)
assert request.url == "http://localhost:5000/custom/path/foo"
+ await client.close()
@pytest.mark.parametrize(
"client",
@@ -1522,7 +1559,7 @@ def test_base_url_trailing_slash(self, client: AsyncHyperspell) -> None:
],
ids=["standard", "custom http client"],
)
- def test_base_url_no_trailing_slash(self, client: AsyncHyperspell) -> None:
+ async def test_base_url_no_trailing_slash(self, client: AsyncHyperspell) -> None:
request = client._build_request(
FinalRequestOptions(
method="post",
@@ -1531,6 +1568,7 @@ def test_base_url_no_trailing_slash(self, client: AsyncHyperspell) -> None:
),
)
assert request.url == "http://localhost:5000/custom/path/foo"
+ await client.close()
@pytest.mark.parametrize(
"client",
@@ -1551,7 +1589,7 @@ def test_base_url_no_trailing_slash(self, client: AsyncHyperspell) -> None:
],
ids=["standard", "custom http client"],
)
- def test_absolute_request_url(self, client: AsyncHyperspell) -> None:
+ async def test_absolute_request_url(self, client: AsyncHyperspell) -> None:
request = client._build_request(
FinalRequestOptions(
method="post",
@@ -1560,37 +1598,43 @@ def test_absolute_request_url(self, client: AsyncHyperspell) -> None:
),
)
assert request.url == "https://myapi.com/foo"
+ await client.close()
async def test_copied_client_does_not_close_http(self) -> None:
- client = AsyncHyperspell(base_url=base_url, api_key=api_key, user_id=user_id, _strict_response_validation=True)
- assert not client.is_closed()
+ test_client = AsyncHyperspell(
+ base_url=base_url, api_key=api_key, user_id=user_id, _strict_response_validation=True
+ )
+ assert not test_client.is_closed()
- copied = client.copy()
- assert copied is not client
+ copied = test_client.copy()
+ assert copied is not test_client
del copied
await asyncio.sleep(0.2)
- assert not client.is_closed()
+ assert not test_client.is_closed()
async def test_client_context_manager(self) -> None:
- client = AsyncHyperspell(base_url=base_url, api_key=api_key, user_id=user_id, _strict_response_validation=True)
- async with client as c2:
- assert c2 is client
+ test_client = AsyncHyperspell(
+ base_url=base_url, api_key=api_key, user_id=user_id, _strict_response_validation=True
+ )
+ async with test_client as c2:
+ assert c2 is test_client
assert not c2.is_closed()
- assert not client.is_closed()
- assert client.is_closed()
+ assert not test_client.is_closed()
+ assert test_client.is_closed()
@pytest.mark.respx(base_url=base_url)
- @pytest.mark.asyncio
- async def test_client_response_validation_error(self, respx_mock: MockRouter) -> None:
+ async def test_client_response_validation_error(
+ self, respx_mock: MockRouter, async_client: AsyncHyperspell
+ ) -> None:
class Model(BaseModel):
foo: str
respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}}))
with pytest.raises(APIResponseValidationError) as exc:
- await self.client.get("/foo", cast_to=Model)
+ await async_client.get("/foo", cast_to=Model)
assert isinstance(exc.value.__cause__, ValidationError)
@@ -1605,7 +1649,6 @@ async def test_client_max_retries_validation(self) -> None:
)
@pytest.mark.respx(base_url=base_url)
- @pytest.mark.asyncio
async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None:
class Model(BaseModel):
name: str
@@ -1619,11 +1662,16 @@ class Model(BaseModel):
with pytest.raises(APIResponseValidationError):
await strict_client.get("/foo", cast_to=Model)
- client = AsyncHyperspell(base_url=base_url, api_key=api_key, user_id=user_id, _strict_response_validation=False)
+ non_strict_client = AsyncHyperspell(
+ base_url=base_url, api_key=api_key, user_id=user_id, _strict_response_validation=False
+ )
- response = await client.get("/foo", cast_to=Model)
+ response = await non_strict_client.get("/foo", cast_to=Model)
assert isinstance(response, str) # type: ignore[unreachable]
+ await strict_client.close()
+ await non_strict_client.close()
+
@pytest.mark.parametrize(
"remaining_retries,retry_after,timeout",
[
@@ -1646,13 +1694,12 @@ class Model(BaseModel):
],
)
@mock.patch("time.time", mock.MagicMock(return_value=1696004797))
- @pytest.mark.asyncio
- async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None:
- client = AsyncHyperspell(base_url=base_url, api_key=api_key, user_id=user_id, _strict_response_validation=True)
-
+ async def test_parse_retry_after_header(
+ self, remaining_retries: int, retry_after: str, timeout: float, async_client: AsyncHyperspell
+ ) -> None:
headers = httpx.Headers({"retry-after": retry_after})
options = FinalRequestOptions(method="get", url="/foo", max_retries=3)
- calculated = client._calculate_retry_timeout(remaining_retries, options, headers)
+ calculated = async_client._calculate_retry_timeout(remaining_retries, options, headers)
assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType]
@mock.patch("hyperspell._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@@ -1665,7 +1712,7 @@ async def test_retrying_timeout_errors_doesnt_leak(
with pytest.raises(APITimeoutError):
await async_client.memories.with_streaming_response.add(text="text").__aenter__()
- assert _get_open_connections(self.client) == 0
+ assert _get_open_connections(async_client) == 0
@mock.patch("hyperspell._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
@@ -1676,12 +1723,11 @@ async def test_retrying_status_errors_doesnt_leak(
with pytest.raises(APIStatusError):
await async_client.memories.with_streaming_response.add(text="text").__aenter__()
- assert _get_open_connections(self.client) == 0
+ assert _get_open_connections(async_client) == 0
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
@mock.patch("hyperspell._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
- @pytest.mark.asyncio
@pytest.mark.parametrize("failure_mode", ["status", "exception"])
async def test_retries_taken(
self,
@@ -1713,7 +1759,6 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
@mock.patch("hyperspell._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
- @pytest.mark.asyncio
async def test_omit_retry_count_header(
self, async_client: AsyncHyperspell, failures_before_success: int, respx_mock: MockRouter
) -> None:
@@ -1739,7 +1784,6 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
@mock.patch("hyperspell._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
@pytest.mark.respx(base_url=base_url)
- @pytest.mark.asyncio
async def test_overwrite_retry_count_header(
self, async_client: AsyncHyperspell, failures_before_success: int, respx_mock: MockRouter
) -> None:
@@ -1789,26 +1833,26 @@ async def test_default_client_creation(self) -> None:
)
@pytest.mark.respx(base_url=base_url)
- async def test_follow_redirects(self, respx_mock: MockRouter) -> None:
+ async def test_follow_redirects(self, respx_mock: MockRouter, async_client: AsyncHyperspell) -> 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)
+ response = await async_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:
+ async def test_follow_redirects_disabled(self, respx_mock: MockRouter, async_client: AsyncHyperspell) -> 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(
+ await async_client.post(
"/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
)
From 516dd5e4f13fcffa9593e58f7a4c8bf855968ae0 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 4 Nov 2025 06:27:23 +0000
Subject: [PATCH 11/28] chore(internal): grammar fix (it's -> its)
---
src/hyperspell/_utils/_utils.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/hyperspell/_utils/_utils.py b/src/hyperspell/_utils/_utils.py
index 50d59269..eec7f4a1 100644
--- a/src/hyperspell/_utils/_utils.py
+++ b/src/hyperspell/_utils/_utils.py
@@ -133,7 +133,7 @@ def is_given(obj: _T | NotGiven | Omit) -> TypeGuard[_T]:
# Type safe methods for narrowing types with TypeVars.
# The default narrowing for isinstance(obj, dict) is dict[unknown, unknown],
# however this cause Pyright to rightfully report errors. As we know we don't
-# care about the contained types we can safely use `object` in it's place.
+# care about the contained types we can safely use `object` in its place.
#
# There are two separate functions defined, `is_*` and `is_*_t` for different use cases.
# `is_*` is for when you're dealing with an unknown input
From 2e9f356bd097204a9d89b927768e261e79397b36 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 6 Nov 2025 06:32:03 +0000
Subject: [PATCH 12/28] feat(api): api update
---
.stats.yml | 4 ++--
src/hyperspell/types/connection_list_response.py | 3 +++
src/hyperspell/types/integration_list_response.py | 9 +++++++++
3 files changed, 14 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index bb2dbeb0..e63cd527 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 21
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-a08047fff67bf0bb9fd8223baba5091eea8477e1933954222e0a690ddce41d5c.yml
-openapi_spec_hash: 7d2a75a868b25e22515a73835d7e13da
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-b32b07c9efb82b2bc14d0c76a74097cc4465884e3c1bd64525a16efb36f176b6.yml
+openapi_spec_hash: 3ec8ccf486481cf56b1625814aa04d72
config_hash: ca45358b5407440488ec988e3ee21412
diff --git a/src/hyperspell/types/connection_list_response.py b/src/hyperspell/types/connection_list_response.py
index 95c9ab19..0a60e0e6 100644
--- a/src/hyperspell/types/connection_list_response.py
+++ b/src/hyperspell/types/connection_list_response.py
@@ -12,6 +12,9 @@ class Connection(BaseModel):
id: str
"""The connection's id"""
+ integration_id: str
+ """The connection's integration id"""
+
label: Optional[str] = None
"""The connection's label"""
diff --git a/src/hyperspell/types/integration_list_response.py b/src/hyperspell/types/integration_list_response.py
index 36c8fed7..c189654f 100644
--- a/src/hyperspell/types/integration_list_response.py
+++ b/src/hyperspell/types/integration_list_response.py
@@ -15,6 +15,15 @@ class Integration(BaseModel):
allow_multiple_connections: bool
"""Whether the integration allows multiple connections"""
+ auth_provider: Literal["nango", "hyperspell", "composio", "whitelabel", "unified"]
+ """The integration's auth provider"""
+
+ icon: str
+ """Generate a display name from the provider by capitalizing each word."""
+
+ name: str
+ """Generate a display name from the provider by capitalizing each word."""
+
provider: Literal[
"collections",
"vault",
From 72522026ca0eb7b2ab24101d88b1add5f92b1501 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 6 Nov 2025 10:32:04 +0000
Subject: [PATCH 13/28] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index e63cd527..0fb2f8d6 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 21
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-b32b07c9efb82b2bc14d0c76a74097cc4465884e3c1bd64525a16efb36f176b6.yml
-openapi_spec_hash: 3ec8ccf486481cf56b1625814aa04d72
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-689fa821bc106e236bde463b0a1898788ce8913e70215a7ba49aa8f487115c5c.yml
+openapi_spec_hash: 1f5a6e36a198acd72308a315a042daa7
config_hash: ca45358b5407440488ec988e3ee21412
From 4d8d0a501af22b0d5321e34e1d6c368dec622d34 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 11 Nov 2025 06:15:49 +0000
Subject: [PATCH 14/28] chore(package): drop Python 3.8 support
---
README.md | 4 ++--
pyproject.toml | 5 ++---
src/hyperspell/_utils/_sync.py | 34 +++-------------------------------
3 files changed, 7 insertions(+), 36 deletions(-)
diff --git a/README.md b/README.md
index c94d2be3..df75e098 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
[)](https://pypi.org/project/hyperspell/)
-The Hyperspell Python library provides convenient access to the Hyperspell REST API from any Python 3.8+
+The Hyperspell Python library provides convenient access to the Hyperspell REST API from any Python 3.9+
application. The library includes type definitions for all request params and response fields,
and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx).
@@ -476,7 +476,7 @@ print(hyperspell.__version__)
## Requirements
-Python 3.8 or higher.
+Python 3.9 or higher.
## Contributing
diff --git a/pyproject.toml b/pyproject.toml
index ddc8761f..7bacae83 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,11 +15,10 @@ dependencies = [
"distro>=1.7.0, <2",
"sniffio",
]
-requires-python = ">= 3.8"
+requires-python = ">= 3.9"
classifiers = [
"Typing :: Typed",
"Intended Audience :: Developers",
- "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
@@ -141,7 +140,7 @@ filterwarnings = [
# there are a couple of flags that are still disabled by
# default in strict mode as they are experimental and niche.
typeCheckingMode = "strict"
-pythonVersion = "3.8"
+pythonVersion = "3.9"
exclude = [
"_dev",
diff --git a/src/hyperspell/_utils/_sync.py b/src/hyperspell/_utils/_sync.py
index ad7ec71b..f6027c18 100644
--- a/src/hyperspell/_utils/_sync.py
+++ b/src/hyperspell/_utils/_sync.py
@@ -1,10 +1,8 @@
from __future__ import annotations
-import sys
import asyncio
import functools
-import contextvars
-from typing import Any, TypeVar, Callable, Awaitable
+from typing import TypeVar, Callable, Awaitable
from typing_extensions import ParamSpec
import anyio
@@ -15,34 +13,11 @@
T_ParamSpec = ParamSpec("T_ParamSpec")
-if sys.version_info >= (3, 9):
- _asyncio_to_thread = asyncio.to_thread
-else:
- # backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread
- # for Python 3.8 support
- async def _asyncio_to_thread(
- func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs
- ) -> Any:
- """Asynchronously run function *func* in a separate thread.
-
- Any *args and **kwargs supplied for this function are directly passed
- to *func*. Also, the current :class:`contextvars.Context` is propagated,
- allowing context variables from the main thread to be accessed in the
- separate thread.
-
- Returns a coroutine that can be awaited to get the eventual result of *func*.
- """
- loop = asyncio.events.get_running_loop()
- ctx = contextvars.copy_context()
- func_call = functools.partial(ctx.run, func, *args, **kwargs)
- return await loop.run_in_executor(None, func_call)
-
-
async def to_thread(
func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs
) -> T_Retval:
if sniffio.current_async_library() == "asyncio":
- return await _asyncio_to_thread(func, *args, **kwargs)
+ return await asyncio.to_thread(func, *args, **kwargs)
return await anyio.to_thread.run_sync(
functools.partial(func, *args, **kwargs),
@@ -53,10 +28,7 @@ async def to_thread(
def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]:
"""
Take a blocking function and create an async one that receives the same
- positional and keyword arguments. For python version 3.9 and above, it uses
- asyncio.to_thread to run the function in a separate thread. For python version
- 3.8, it uses locally defined copy of the asyncio.to_thread function which was
- introduced in python 3.9.
+ positional and keyword arguments.
Usage:
From 53fdd97e8631bf9093c775de401b0f1fd8965efe Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 11 Nov 2025 06:16:20 +0000
Subject: [PATCH 15/28] fix: compat with Python 3.14
---
src/hyperspell/_models.py | 11 ++++++++---
tests/test_models.py | 8 ++++----
2 files changed, 12 insertions(+), 7 deletions(-)
diff --git a/src/hyperspell/_models.py b/src/hyperspell/_models.py
index 6a3cd1d2..fcec2cf9 100644
--- a/src/hyperspell/_models.py
+++ b/src/hyperspell/_models.py
@@ -2,6 +2,7 @@
import os
import inspect
+import weakref
from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast
from datetime import date, datetime
from typing_extensions import (
@@ -573,6 +574,9 @@ class CachedDiscriminatorType(Protocol):
__discriminator__: DiscriminatorDetails
+DISCRIMINATOR_CACHE: weakref.WeakKeyDictionary[type, DiscriminatorDetails] = weakref.WeakKeyDictionary()
+
+
class DiscriminatorDetails:
field_name: str
"""The name of the discriminator field in the variant class, e.g.
@@ -615,8 +619,9 @@ def __init__(
def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, ...]) -> DiscriminatorDetails | None:
- if isinstance(union, CachedDiscriminatorType):
- return union.__discriminator__
+ cached = DISCRIMINATOR_CACHE.get(union)
+ if cached is not None:
+ return cached
discriminator_field_name: str | None = None
@@ -669,7 +674,7 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any,
discriminator_field=discriminator_field_name,
discriminator_alias=discriminator_alias,
)
- cast(CachedDiscriminatorType, union).__discriminator__ = details
+ DISCRIMINATOR_CACHE.setdefault(union, details)
return details
diff --git a/tests/test_models.py b/tests/test_models.py
index 088404f1..88942a41 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -9,7 +9,7 @@
from hyperspell._utils import PropertyInfo
from hyperspell._compat import PYDANTIC_V1, parse_obj, model_dump, model_json
-from hyperspell._models import BaseModel, construct_type
+from hyperspell._models import DISCRIMINATOR_CACHE, BaseModel, construct_type
class BasicModel(BaseModel):
@@ -809,7 +809,7 @@ class B(BaseModel):
UnionType = cast(Any, Union[A, B])
- assert not hasattr(UnionType, "__discriminator__")
+ assert not DISCRIMINATOR_CACHE.get(UnionType)
m = construct_type(
value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")])
@@ -818,7 +818,7 @@ class B(BaseModel):
assert m.type == "b"
assert m.data == "foo" # type: ignore[comparison-overlap]
- discriminator = UnionType.__discriminator__
+ discriminator = DISCRIMINATOR_CACHE.get(UnionType)
assert discriminator is not None
m = construct_type(
@@ -830,7 +830,7 @@ class B(BaseModel):
# if the discriminator details object stays the same between invocations then
# we hit the cache
- assert UnionType.__discriminator__ is discriminator
+ assert DISCRIMINATOR_CACHE.get(UnionType) is discriminator
@pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1")
From 6ed583ec067269fb1c27c8dcce9167795a1abd17 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 12 Nov 2025 05:50:06 +0000
Subject: [PATCH 16/28] fix(compat): update signatures of `model_dump` and
`model_dump_json` for Pydantic v1
---
src/hyperspell/_models.py | 41 +++++++++++++++++++++++++++------------
1 file changed, 29 insertions(+), 12 deletions(-)
diff --git a/src/hyperspell/_models.py b/src/hyperspell/_models.py
index fcec2cf9..ca9500b2 100644
--- a/src/hyperspell/_models.py
+++ b/src/hyperspell/_models.py
@@ -257,15 +257,16 @@ def model_dump(
mode: Literal["json", "python"] | str = "python",
include: IncEx | None = None,
exclude: IncEx | None = None,
+ context: Any | None = None,
by_alias: bool | None = None,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
+ exclude_computed_fields: bool = False,
round_trip: bool = False,
warnings: bool | Literal["none", "warn", "error"] = True,
- context: dict[str, Any] | None = None,
- serialize_as_any: bool = False,
fallback: Callable[[Any], Any] | None = None,
+ serialize_as_any: bool = False,
) -> dict[str, Any]:
"""Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump
@@ -273,16 +274,24 @@ def model_dump(
Args:
mode: The mode in which `to_python` should run.
- If mode is 'json', the dictionary will only contain JSON serializable types.
- If mode is 'python', the dictionary may contain any Python objects.
- include: A list of fields to include in the output.
- exclude: A list of fields to exclude from the output.
+ If mode is 'json', the output will only contain JSON serializable types.
+ If mode is 'python', the output may contain non-JSON-serializable Python objects.
+ include: A set of fields to include in the output.
+ exclude: A set of fields to exclude from the output.
+ context: Additional context to pass to the serializer.
by_alias: Whether to use the field's alias in the dictionary key if defined.
- exclude_unset: Whether to exclude fields that are unset or None from the output.
- exclude_defaults: Whether to exclude fields that are set to their default value from the output.
- exclude_none: Whether to exclude fields that have a value of `None` from the output.
- round_trip: Whether to enable serialization and deserialization round-trip support.
- warnings: Whether to log warnings when invalid fields are encountered.
+ exclude_unset: Whether to exclude fields that have not been explicitly set.
+ exclude_defaults: Whether to exclude fields that are set to their default value.
+ exclude_none: Whether to exclude fields that have a value of `None`.
+ exclude_computed_fields: Whether to exclude computed fields.
+ While this can be useful for round-tripping, it is usually recommended to use the dedicated
+ `round_trip` parameter instead.
+ round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T].
+ warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors,
+ "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError].
+ fallback: A function to call when an unknown value is encountered. If not provided,
+ a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised.
+ serialize_as_any: Whether to serialize fields with duck-typing serialization behavior.
Returns:
A dictionary representation of the model.
@@ -299,6 +308,8 @@ def model_dump(
raise ValueError("serialize_as_any is only supported in Pydantic v2")
if fallback is not None:
raise ValueError("fallback is only supported in Pydantic v2")
+ if exclude_computed_fields != False:
+ raise ValueError("exclude_computed_fields is only supported in Pydantic v2")
dumped = super().dict( # pyright: ignore[reportDeprecated]
include=include,
exclude=exclude,
@@ -315,15 +326,17 @@ def model_dump_json(
self,
*,
indent: int | None = None,
+ ensure_ascii: bool = False,
include: IncEx | None = None,
exclude: IncEx | None = None,
+ context: Any | None = None,
by_alias: bool | None = None,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
+ exclude_computed_fields: bool = False,
round_trip: bool = False,
warnings: bool | Literal["none", "warn", "error"] = True,
- context: dict[str, Any] | None = None,
fallback: Callable[[Any], Any] | None = None,
serialize_as_any: bool = False,
) -> str:
@@ -355,6 +368,10 @@ def model_dump_json(
raise ValueError("serialize_as_any is only supported in Pydantic v2")
if fallback is not None:
raise ValueError("fallback is only supported in Pydantic v2")
+ if ensure_ascii != False:
+ raise ValueError("ensure_ascii is only supported in Pydantic v2")
+ if exclude_computed_fields != False:
+ raise ValueError("exclude_computed_fields is only supported in Pydantic v2")
return super().json( # type: ignore[reportDeprecated]
indent=indent,
include=include,
From 75b429e1dc942a6595024682d7bf9d7b22cad632 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 22 Nov 2025 05:14:14 +0000
Subject: [PATCH 17/28] chore: add Python 3.14 classifier and testing
---
pyproject.toml | 1 +
1 file changed, 1 insertion(+)
diff --git a/pyproject.toml b/pyproject.toml
index 7bacae83..ee3d7b59 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -24,6 +24,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
"Operating System :: OS Independent",
"Operating System :: POSIX",
"Operating System :: MacOS",
From c6760434add9e200c865154a08d0289d556b70a8 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 28 Nov 2025 03:49:38 +0000
Subject: [PATCH 18/28] fix: ensure streams are always closed
---
src/hyperspell/_streaming.py | 22 ++++++++++++----------
1 file changed, 12 insertions(+), 10 deletions(-)
diff --git a/src/hyperspell/_streaming.py b/src/hyperspell/_streaming.py
index c3eb77e6..11fc4ffe 100644
--- a/src/hyperspell/_streaming.py
+++ b/src/hyperspell/_streaming.py
@@ -54,11 +54,12 @@ def __stream__(self) -> Iterator[_T]:
process_data = self._client._process_response_data
iterator = self._iter_events()
- for sse in iterator:
- yield process_data(data=sse.json(), cast_to=cast_to, response=response)
-
- # As we might not fully consume the response stream, we need to close it explicitly
- response.close()
+ try:
+ for sse in iterator:
+ yield process_data(data=sse.json(), cast_to=cast_to, response=response)
+ finally:
+ # Ensure the response is closed even if the consumer doesn't read all data
+ response.close()
def __enter__(self) -> Self:
return self
@@ -117,11 +118,12 @@ async def __stream__(self) -> AsyncIterator[_T]:
process_data = self._client._process_response_data
iterator = self._iter_events()
- async for sse in iterator:
- yield process_data(data=sse.json(), cast_to=cast_to, response=response)
-
- # As we might not fully consume the response stream, we need to close it explicitly
- await response.aclose()
+ try:
+ async for sse in iterator:
+ yield process_data(data=sse.json(), cast_to=cast_to, response=response)
+ finally:
+ # Ensure the response is closed even if the consumer doesn't read all data
+ await response.aclose()
async def __aenter__(self) -> Self:
return self
From 223b7fcd14499af7a908d7ddb9cbe83e44656737 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 28 Nov 2025 03:50:42 +0000
Subject: [PATCH 19/28] chore(deps): mypy 1.18.1 has a regression, pin to 1.17
---
pyproject.toml | 2 +-
requirements-dev.lock | 4 +++-
requirements.lock | 8 ++++----
3 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index ee3d7b59..147fb5d2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -46,7 +46,7 @@ managed = true
# version pins are in requirements-dev.lock
dev-dependencies = [
"pyright==1.1.399",
- "mypy",
+ "mypy==1.17",
"respx",
"pytest",
"pytest-asyncio",
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 40bddc70..d397ac50 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -72,7 +72,7 @@ mdurl==0.1.2
multidict==6.4.4
# via aiohttp
# via yarl
-mypy==1.14.1
+mypy==1.17.0
mypy-extensions==1.0.0
# via mypy
nodeenv==1.8.0
@@ -81,6 +81,8 @@ nox==2023.4.22
packaging==23.2
# via nox
# via pytest
+pathspec==0.12.1
+ # via mypy
platformdirs==3.11.0
# via virtualenv
pluggy==1.5.0
diff --git a/requirements.lock b/requirements.lock
index 0a0fdef1..2c7a3405 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -55,21 +55,21 @@ multidict==6.4.4
propcache==0.3.1
# via aiohttp
# via yarl
-pydantic==2.11.9
+pydantic==2.12.5
# via hyperspell
-pydantic-core==2.33.2
+pydantic-core==2.41.5
# via pydantic
sniffio==1.3.0
# via anyio
# via hyperspell
-typing-extensions==4.12.2
+typing-extensions==4.15.0
# via anyio
# via hyperspell
# via multidict
# via pydantic
# via pydantic-core
# via typing-inspection
-typing-inspection==0.4.1
+typing-inspection==0.4.2
# via pydantic
yarl==1.20.0
# via aiohttp
From 9170730ae0dd8259dcd9b5814b76e6a79247b237 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 3 Dec 2025 08:04:08 +0000
Subject: [PATCH 20/28] chore: update lockfile
---
pyproject.toml | 14 +++---
requirements-dev.lock | 108 +++++++++++++++++++++++-------------------
requirements.lock | 31 ++++++------
3 files changed, 83 insertions(+), 70 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 147fb5d2..0853d986 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,14 +7,16 @@ license = "MIT"
authors = [
{ name = "Hyperspell", email = "hello@hyperspell.com" },
]
+
dependencies = [
- "httpx>=0.23.0, <1",
- "pydantic>=1.9.0, <3",
- "typing-extensions>=4.10, <5",
- "anyio>=3.5.0, <5",
- "distro>=1.7.0, <2",
- "sniffio",
+ "httpx>=0.23.0, <1",
+ "pydantic>=1.9.0, <3",
+ "typing-extensions>=4.10, <5",
+ "anyio>=3.5.0, <5",
+ "distro>=1.7.0, <2",
+ "sniffio",
]
+
requires-python = ">= 3.9"
classifiers = [
"Typing :: Typed",
diff --git a/requirements-dev.lock b/requirements-dev.lock
index d397ac50..26605768 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -12,40 +12,45 @@
-e file:.
aiohappyeyeballs==2.6.1
# via aiohttp
-aiohttp==3.12.8
+aiohttp==3.13.2
# via httpx-aiohttp
# via hyperspell
-aiosignal==1.3.2
+aiosignal==1.4.0
# via aiohttp
-annotated-types==0.6.0
+annotated-types==0.7.0
# via pydantic
-anyio==4.4.0
+anyio==4.12.0
# via httpx
# via hyperspell
-argcomplete==3.1.2
+argcomplete==3.6.3
# via nox
async-timeout==5.0.1
# via aiohttp
-attrs==25.3.0
+attrs==25.4.0
# via aiohttp
-certifi==2023.7.22
+ # via nox
+backports-asyncio-runner==1.2.0
+ # via pytest-asyncio
+certifi==2025.11.12
# via httpcore
# via httpx
-colorlog==6.7.0
+colorlog==6.10.1
+ # via nox
+dependency-groups==1.3.1
# via nox
-dirty-equals==0.6.0
-distlib==0.3.7
+dirty-equals==0.11
+distlib==0.4.0
# via virtualenv
-distro==1.8.0
+distro==1.9.0
# via hyperspell
-exceptiongroup==1.2.2
+exceptiongroup==1.3.1
# via anyio
# via pytest
-execnet==2.1.1
+execnet==2.1.2
# via pytest-xdist
-filelock==3.12.4
+filelock==3.19.1
# via virtualenv
-frozenlist==1.6.2
+frozenlist==1.8.0
# via aiohttp
# via aiosignal
h11==0.16.0
@@ -58,82 +63,87 @@ httpx==0.28.1
# via respx
httpx-aiohttp==0.1.9
# via hyperspell
-idna==3.4
+humanize==4.13.0
+ # via nox
+idna==3.11
# via anyio
# via httpx
# via yarl
-importlib-metadata==7.0.0
-iniconfig==2.0.0
+importlib-metadata==8.7.0
+iniconfig==2.1.0
# via pytest
markdown-it-py==3.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
-multidict==6.4.4
+multidict==6.7.0
# via aiohttp
# via yarl
mypy==1.17.0
-mypy-extensions==1.0.0
+mypy-extensions==1.1.0
# via mypy
-nodeenv==1.8.0
+nodeenv==1.9.1
# via pyright
-nox==2023.4.22
-packaging==23.2
+nox==2025.11.12
+packaging==25.0
+ # via dependency-groups
# via nox
# via pytest
pathspec==0.12.1
# via mypy
-platformdirs==3.11.0
+platformdirs==4.4.0
# via virtualenv
-pluggy==1.5.0
+pluggy==1.6.0
# via pytest
-propcache==0.3.1
+propcache==0.4.1
# via aiohttp
# via yarl
-pydantic==2.11.9
+pydantic==2.12.5
# via hyperspell
-pydantic-core==2.33.2
+pydantic-core==2.41.5
# via pydantic
-pygments==2.18.0
+pygments==2.19.2
+ # via pytest
# via rich
pyright==1.1.399
-pytest==8.3.3
+pytest==8.4.2
# via pytest-asyncio
# via pytest-xdist
-pytest-asyncio==0.24.0
-pytest-xdist==3.7.0
-python-dateutil==2.8.2
+pytest-asyncio==1.2.0
+pytest-xdist==3.8.0
+python-dateutil==2.9.0.post0
# via time-machine
-pytz==2023.3.post1
- # via dirty-equals
respx==0.22.0
-rich==13.7.1
-ruff==0.9.4
-setuptools==68.2.2
- # via nodeenv
-six==1.16.0
+rich==14.2.0
+ruff==0.14.7
+six==1.17.0
# via python-dateutil
-sniffio==1.3.0
- # via anyio
+sniffio==1.3.1
# via hyperspell
-time-machine==2.9.0
-tomli==2.0.2
+time-machine==2.19.0
+tomli==2.3.0
+ # via dependency-groups
# via mypy
+ # via nox
# via pytest
-typing-extensions==4.12.2
+typing-extensions==4.15.0
+ # via aiosignal
# via anyio
+ # via exceptiongroup
# via hyperspell
# via multidict
# via mypy
# via pydantic
# via pydantic-core
# via pyright
+ # via pytest-asyncio
# via typing-inspection
-typing-inspection==0.4.1
+ # via virtualenv
+typing-inspection==0.4.2
# via pydantic
-virtualenv==20.24.5
+virtualenv==20.35.4
# via nox
-yarl==1.20.0
+yarl==1.22.0
# via aiohttp
-zipp==3.17.0
+zipp==3.23.0
# via importlib-metadata
diff --git a/requirements.lock b/requirements.lock
index 2c7a3405..313b1a01 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -12,28 +12,28 @@
-e file:.
aiohappyeyeballs==2.6.1
# via aiohttp
-aiohttp==3.12.8
+aiohttp==3.13.2
# via httpx-aiohttp
# via hyperspell
-aiosignal==1.3.2
+aiosignal==1.4.0
# via aiohttp
-annotated-types==0.6.0
+annotated-types==0.7.0
# via pydantic
-anyio==4.4.0
+anyio==4.12.0
# via httpx
# via hyperspell
async-timeout==5.0.1
# via aiohttp
-attrs==25.3.0
+attrs==25.4.0
# via aiohttp
-certifi==2023.7.22
+certifi==2025.11.12
# via httpcore
# via httpx
-distro==1.8.0
+distro==1.9.0
# via hyperspell
-exceptiongroup==1.2.2
+exceptiongroup==1.3.1
# via anyio
-frozenlist==1.6.2
+frozenlist==1.8.0
# via aiohttp
# via aiosignal
h11==0.16.0
@@ -45,25 +45,26 @@ httpx==0.28.1
# via hyperspell
httpx-aiohttp==0.1.9
# via hyperspell
-idna==3.4
+idna==3.11
# via anyio
# via httpx
# via yarl
-multidict==6.4.4
+multidict==6.7.0
# via aiohttp
# via yarl
-propcache==0.3.1
+propcache==0.4.1
# via aiohttp
# via yarl
pydantic==2.12.5
# via hyperspell
pydantic-core==2.41.5
# via pydantic
-sniffio==1.3.0
- # via anyio
+sniffio==1.3.1
# via hyperspell
typing-extensions==4.15.0
+ # via aiosignal
# via anyio
+ # via exceptiongroup
# via hyperspell
# via multidict
# via pydantic
@@ -71,5 +72,5 @@ typing-extensions==4.15.0
# via typing-inspection
typing-inspection==0.4.2
# via pydantic
-yarl==1.20.0
+yarl==1.22.0
# via aiohttp
From 0e42e5d97de61b3224eeaac980df5efc35a9336e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 3 Dec 2025 08:12:50 +0000
Subject: [PATCH 21/28] chore(docs): use environment variables for
authentication in code snippets
---
README.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index df75e098..ef641e61 100644
--- a/README.md
+++ b/README.md
@@ -83,6 +83,7 @@ pip install hyperspell[aiohttp]
Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`:
```python
+import os
import asyncio
from hyperspell import DefaultAioHttpClient
from hyperspell import AsyncHyperspell
@@ -90,7 +91,7 @@ from hyperspell import AsyncHyperspell
async def main() -> None:
async with AsyncHyperspell(
- api_key="My API Key",
+ api_key=os.environ.get("HYPERSPELL_TOKEN"), # This is the default and can be omitted
http_client=DefaultAioHttpClient(),
) as client:
memory_status = await client.memories.add(
From 9c7824f603cd5b1935ee1aac676e3fcf299d51f6 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 9 Dec 2025 05:46:38 +0000
Subject: [PATCH 22/28] fix(types): allow pyright to infer TypedDict types
within SequenceNotStr
---
src/hyperspell/_types.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/hyperspell/_types.py b/src/hyperspell/_types.py
index 5f369eea..59d3b796 100644
--- a/src/hyperspell/_types.py
+++ b/src/hyperspell/_types.py
@@ -243,6 +243,9 @@ class HttpxSendArgs(TypedDict, total=False):
if TYPE_CHECKING:
# This works because str.__contains__ does not accept object (either in typeshed or at runtime)
# https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285
+ #
+ # Note: index() and count() methods are intentionally omitted to allow pyright to properly
+ # infer TypedDict types when dict literals are used in lists assigned to SequenceNotStr.
class SequenceNotStr(Protocol[_T_co]):
@overload
def __getitem__(self, index: SupportsIndex, /) -> _T_co: ...
@@ -251,8 +254,6 @@ def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ...
def __contains__(self, value: object, /) -> bool: ...
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[_T_co]: ...
- def index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ...
- def count(self, value: Any, /) -> int: ...
def __reversed__(self) -> Iterator[_T_co]: ...
else:
# just point this to a normal `Sequence` at runtime to avoid having to special case
From 16f09b68057a3be4b878f32980d7d07fefbc7cb9 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 9 Dec 2025 05:48:38 +0000
Subject: [PATCH 23/28] chore: add missing docstrings
---
src/hyperspell/types/auth_me_response.py | 2 ++
src/hyperspell/types/memory_search_params.py | 20 ++++++++++++++++++++
2 files changed, 22 insertions(+)
diff --git a/src/hyperspell/types/auth_me_response.py b/src/hyperspell/types/auth_me_response.py
index d4ca2c54..7eee2ac6 100644
--- a/src/hyperspell/types/auth_me_response.py
+++ b/src/hyperspell/types/auth_me_response.py
@@ -10,6 +10,8 @@
class App(BaseModel):
+ """The Hyperspell app's id this user belongs to"""
+
id: str
"""The Hyperspell app's id this user belongs to"""
diff --git a/src/hyperspell/types/memory_search_params.py b/src/hyperspell/types/memory_search_params.py
index 0e1dd1ca..571b1249 100644
--- a/src/hyperspell/types/memory_search_params.py
+++ b/src/hyperspell/types/memory_search_params.py
@@ -91,6 +91,8 @@ class MemorySearchParams(TypedDict, total=False):
class OptionsBox(TypedDict, total=False):
+ """Search options for Box"""
+
after: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
"""Only query documents created on or after this date."""
@@ -107,6 +109,8 @@ class OptionsBox(TypedDict, total=False):
class OptionsCollections(TypedDict, total=False):
+ """Search options for vault"""
+
after: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
"""Only query documents created on or after this date."""
@@ -123,6 +127,8 @@ class OptionsCollections(TypedDict, total=False):
class OptionsGoogleCalendar(TypedDict, total=False):
+ """Search options for Google Calendar"""
+
after: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
"""Only query documents created on or after this date."""
@@ -146,6 +152,8 @@ class OptionsGoogleCalendar(TypedDict, total=False):
class OptionsGoogleDrive(TypedDict, total=False):
+ """Search options for Google Drive"""
+
after: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
"""Only query documents created on or after this date."""
@@ -162,6 +170,8 @@ class OptionsGoogleDrive(TypedDict, total=False):
class OptionsGoogleMail(TypedDict, total=False):
+ """Search options for Gmail"""
+
after: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
"""Only query documents created on or after this date."""
@@ -186,6 +196,8 @@ class OptionsGoogleMail(TypedDict, total=False):
class OptionsNotion(TypedDict, total=False):
+ """Search options for Notion"""
+
after: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
"""Only query documents created on or after this date."""
@@ -208,6 +220,8 @@ class OptionsNotion(TypedDict, total=False):
class OptionsReddit(TypedDict, total=False):
+ """Search options for Reddit"""
+
after: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
"""Only query documents created on or after this date."""
@@ -236,6 +250,8 @@ class OptionsReddit(TypedDict, total=False):
class OptionsSlack(TypedDict, total=False):
+ """Search options for Slack"""
+
after: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
"""Only query documents created on or after this date."""
@@ -270,6 +286,8 @@ class OptionsSlack(TypedDict, total=False):
class OptionsWebCrawler(TypedDict, total=False):
+ """Search options for Web Crawler"""
+
after: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
"""Only query documents created on or after this date."""
@@ -292,6 +310,8 @@ class OptionsWebCrawler(TypedDict, total=False):
class Options(TypedDict, total=False):
+ """Search options for the query."""
+
after: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
"""Only query documents created on or after this date."""
From cc4b7a13f9aa8f38e2d115fc2292cf6b1f959cd0 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 12 Dec 2025 05:36:37 +0000
Subject: [PATCH 24/28] feat(api): api update
---
.stats.yml | 4 +-
src/hyperspell/resources/memories.py | 22 ++++++-
src/hyperspell/types/memory_add_params.py | 9 ++-
src/hyperspell/types/memory_search_params.py | 62 +++++++++++++++++++-
src/hyperspell/types/memory_upload_params.py | 7 +++
tests/api_resources/test_memories.py | 24 ++++++++
6 files changed, 123 insertions(+), 5 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 0fb2f8d6..1bb075d8 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 21
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-689fa821bc106e236bde463b0a1898788ce8913e70215a7ba49aa8f487115c5c.yml
-openapi_spec_hash: 1f5a6e36a198acd72308a315a042daa7
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-622b43986c45c1efbeb06dd933786980257f300b7a0edbb2d2a4f708afacce36.yml
+openapi_spec_hash: ade837ffc4873d3b50a0fab3f061b397
config_hash: ca45358b5407440488ec988e3ee21412
diff --git a/src/hyperspell/resources/memories.py b/src/hyperspell/resources/memories.py
index 30820969..dd4c48a7 100644
--- a/src/hyperspell/resources/memories.py
+++ b/src/hyperspell/resources/memories.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from typing import List, Union, Mapping, Optional, cast
+from typing import Dict, List, Union, Mapping, Optional, cast
from datetime import datetime
from typing_extensions import Literal
@@ -257,6 +257,7 @@ def add(
text: str,
collection: Optional[str] | Omit = omit,
date: Union[str, datetime] | Omit = omit,
+ metadata: Optional[Dict[str, Union[str, float, bool]]] | Omit = omit,
resource_id: str | Omit = omit,
title: Optional[str] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -282,6 +283,9 @@ def add(
the date of the last message). This helps the ranking algorithm and allows you
to filter by date range.
+ metadata: Custom metadata for filtering. Keys must be alphanumeric with underscores, max
+ 64 chars. Values must be string, number, or boolean.
+
resource_id: The resource ID to add the document to. If not provided, a new resource ID will
be generated. If provided, the document will be updated if it already exists.
@@ -302,6 +306,7 @@ def add(
"text": text,
"collection": collection,
"date": date,
+ "metadata": metadata,
"resource_id": resource_id,
"title": title,
},
@@ -528,6 +533,7 @@ def upload(
*,
file: FileTypes,
collection: Optional[str] | Omit = omit,
+ metadata: Optional[str] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -547,6 +553,9 @@ def upload(
collection: The collection to add the document to.
+ metadata: Custom metadata as JSON string for filtering. Keys must be alphanumeric with
+ underscores, max 64 chars. Values must be string, number, or boolean.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -559,6 +568,7 @@ def upload(
{
"file": file,
"collection": collection,
+ "metadata": metadata,
}
)
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
@@ -803,6 +813,7 @@ async def add(
text: str,
collection: Optional[str] | Omit = omit,
date: Union[str, datetime] | Omit = omit,
+ metadata: Optional[Dict[str, Union[str, float, bool]]] | Omit = omit,
resource_id: str | Omit = omit,
title: Optional[str] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -828,6 +839,9 @@ async def add(
the date of the last message). This helps the ranking algorithm and allows you
to filter by date range.
+ metadata: Custom metadata for filtering. Keys must be alphanumeric with underscores, max
+ 64 chars. Values must be string, number, or boolean.
+
resource_id: The resource ID to add the document to. If not provided, a new resource ID will
be generated. If provided, the document will be updated if it already exists.
@@ -848,6 +862,7 @@ async def add(
"text": text,
"collection": collection,
"date": date,
+ "metadata": metadata,
"resource_id": resource_id,
"title": title,
},
@@ -1074,6 +1089,7 @@ async def upload(
*,
file: FileTypes,
collection: Optional[str] | Omit = omit,
+ metadata: Optional[str] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
@@ -1093,6 +1109,9 @@ async def upload(
collection: The collection to add the document to.
+ metadata: Custom metadata as JSON string for filtering. Keys must be alphanumeric with
+ underscores, max 64 chars. Values must be string, number, or boolean.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -1105,6 +1124,7 @@ async def upload(
{
"file": file,
"collection": collection,
+ "metadata": metadata,
}
)
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
diff --git a/src/hyperspell/types/memory_add_params.py b/src/hyperspell/types/memory_add_params.py
index 5c94f1db..e963f6d2 100644
--- a/src/hyperspell/types/memory_add_params.py
+++ b/src/hyperspell/types/memory_add_params.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from typing import Union, Optional
+from typing import Dict, Union, Optional
from datetime import datetime
from typing_extensions import Required, Annotated, TypedDict
@@ -27,6 +27,13 @@ class MemoryAddParams(TypedDict, total=False):
range.
"""
+ metadata: Optional[Dict[str, Union[str, float, bool]]]
+ """Custom metadata for filtering.
+
+ Keys must be alphanumeric with underscores, max 64 chars. Values must be string,
+ number, or boolean.
+ """
+
resource_id: str
"""The resource ID to add the document to.
diff --git a/src/hyperspell/types/memory_search_params.py b/src/hyperspell/types/memory_search_params.py
index 571b1249..a12d2359 100644
--- a/src/hyperspell/types/memory_search_params.py
+++ b/src/hyperspell/types/memory_search_params.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from typing import List, Union, Optional
+from typing import Dict, List, Union, Optional
from datetime import datetime
from typing_extensions import Literal, Required, Annotated, TypedDict
@@ -99,6 +99,12 @@ class OptionsBox(TypedDict, total=False):
before: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
"""Only query documents created before this date."""
+ filter: Optional[Dict[str, object]]
+ """Metadata filters using MongoDB-style operators.
+
+ Example: {'status': 'published', 'priority': {'$gt': 3}}
+ """
+
weight: float
"""Weight of results from this source.
@@ -117,6 +123,12 @@ class OptionsCollections(TypedDict, total=False):
before: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
"""Only query documents created before this date."""
+ filter: Optional[Dict[str, object]]
+ """Metadata filters using MongoDB-style operators.
+
+ Example: {'status': 'published', 'priority': {'$gt': 3}}
+ """
+
weight: float
"""Weight of results from this source.
@@ -142,6 +154,12 @@ class OptionsGoogleCalendar(TypedDict, total=False):
list of calendars with the `/integrations/google_calendar/list` endpoint.
"""
+ filter: Optional[Dict[str, object]]
+ """Metadata filters using MongoDB-style operators.
+
+ Example: {'status': 'published', 'priority': {'$gt': 3}}
+ """
+
weight: float
"""Weight of results from this source.
@@ -160,6 +178,12 @@ class OptionsGoogleDrive(TypedDict, total=False):
before: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
"""Only query documents created before this date."""
+ filter: Optional[Dict[str, object]]
+ """Metadata filters using MongoDB-style operators.
+
+ Example: {'status': 'published', 'priority': {'$gt': 3}}
+ """
+
weight: float
"""Weight of results from this source.
@@ -178,6 +202,12 @@ class OptionsGoogleMail(TypedDict, total=False):
before: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
"""Only query documents created before this date."""
+ filter: Optional[Dict[str, object]]
+ """Metadata filters using MongoDB-style operators.
+
+ Example: {'status': 'published', 'priority': {'$gt': 3}}
+ """
+
label_ids: SequenceNotStr[str]
"""List of label IDs to filter messages (e.g., ['INBOX', 'SENT', 'DRAFT']).
@@ -204,6 +234,12 @@ class OptionsNotion(TypedDict, total=False):
before: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
"""Only query documents created before this date."""
+ filter: Optional[Dict[str, object]]
+ """Metadata filters using MongoDB-style operators.
+
+ Example: {'status': 'published', 'priority': {'$gt': 3}}
+ """
+
notion_page_ids: SequenceNotStr[str]
"""List of Notion page IDs to search.
@@ -228,6 +264,12 @@ class OptionsReddit(TypedDict, total=False):
before: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
"""Only query documents created before this date."""
+ filter: Optional[Dict[str, object]]
+ """Metadata filters using MongoDB-style operators.
+
+ Example: {'status': 'published', 'priority': {'$gt': 3}}
+ """
+
period: Literal["hour", "day", "week", "month", "year", "all"]
"""The time period to search. Defaults to 'month'."""
@@ -264,6 +306,12 @@ class OptionsSlack(TypedDict, total=False):
exclude_archived: Optional[bool]
"""If set, pass 'exclude_archived' to Slack. If None, omit the param."""
+ filter: Optional[Dict[str, object]]
+ """Metadata filters using MongoDB-style operators.
+
+ Example: {'status': 'published', 'priority': {'$gt': 3}}
+ """
+
include_dms: bool
"""Include direct messages (im) when listing conversations."""
@@ -294,6 +342,12 @@ class OptionsWebCrawler(TypedDict, total=False):
before: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
"""Only query documents created before this date."""
+ filter: Optional[Dict[str, object]]
+ """Metadata filters using MongoDB-style operators.
+
+ Example: {'status': 'published', 'priority': {'$gt': 3}}
+ """
+
max_depth: int
"""Maximum depth to crawl from the starting URL"""
@@ -327,6 +381,12 @@ class Options(TypedDict, total=False):
collections: OptionsCollections
"""Search options for vault"""
+ filter: Optional[Dict[str, object]]
+ """Metadata filters using MongoDB-style operators.
+
+ Example: {'status': 'published', 'priority': {'$gt': 3}}
+ """
+
google_calendar: OptionsGoogleCalendar
"""Search options for Google Calendar"""
diff --git a/src/hyperspell/types/memory_upload_params.py b/src/hyperspell/types/memory_upload_params.py
index 51f93268..7e866983 100644
--- a/src/hyperspell/types/memory_upload_params.py
+++ b/src/hyperspell/types/memory_upload_params.py
@@ -16,3 +16,10 @@ class MemoryUploadParams(TypedDict, total=False):
collection: Optional[str]
"""The collection to add the document to."""
+
+ metadata: Optional[str]
+ """Custom metadata as JSON string for filtering.
+
+ Keys must be alphanumeric with underscores, max 64 chars. Values must be string,
+ number, or boolean.
+ """
diff --git a/tests/api_resources/test_memories.py b/tests/api_resources/test_memories.py
index e65db28c..1dc9b321 100644
--- a/tests/api_resources/test_memories.py
+++ b/tests/api_resources/test_memories.py
@@ -116,6 +116,7 @@ def test_method_add_with_all_params(self, client: Hyperspell) -> None:
text="text",
collection="collection",
date=parse_datetime("2019-12-27T18:11:19.117Z"),
+ metadata={"foo": "string"},
resource_id="resource_id",
title="title",
)
@@ -207,27 +208,33 @@ def test_method_search_with_all_params(self, client: Hyperspell) -> None:
"box": {
"after": parse_datetime("2019-12-27T18:11:19.117Z"),
"before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "filter": {"foo": "bar"},
"weight": 0,
},
"collections": {
"after": parse_datetime("2019-12-27T18:11:19.117Z"),
"before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "filter": {"foo": "bar"},
"weight": 0,
},
+ "filter": {"foo": "bar"},
"google_calendar": {
"after": parse_datetime("2019-12-27T18:11:19.117Z"),
"before": parse_datetime("2019-12-27T18:11:19.117Z"),
"calendar_id": "calendar_id",
+ "filter": {"foo": "bar"},
"weight": 0,
},
"google_drive": {
"after": parse_datetime("2019-12-27T18:11:19.117Z"),
"before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "filter": {"foo": "bar"},
"weight": 0,
},
"google_mail": {
"after": parse_datetime("2019-12-27T18:11:19.117Z"),
"before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "filter": {"foo": "bar"},
"label_ids": ["string"],
"weight": 0,
},
@@ -235,12 +242,14 @@ def test_method_search_with_all_params(self, client: Hyperspell) -> None:
"notion": {
"after": parse_datetime("2019-12-27T18:11:19.117Z"),
"before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "filter": {"foo": "bar"},
"notion_page_ids": ["string"],
"weight": 0,
},
"reddit": {
"after": parse_datetime("2019-12-27T18:11:19.117Z"),
"before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "filter": {"foo": "bar"},
"period": "hour",
"sort": "relevance",
"subreddit": "subreddit",
@@ -251,6 +260,7 @@ def test_method_search_with_all_params(self, client: Hyperspell) -> None:
"before": parse_datetime("2019-12-27T18:11:19.117Z"),
"channels": ["string"],
"exclude_archived": True,
+ "filter": {"foo": "bar"},
"include_dms": True,
"include_group_dms": True,
"include_private": True,
@@ -259,6 +269,7 @@ def test_method_search_with_all_params(self, client: Hyperspell) -> None:
"web_crawler": {
"after": parse_datetime("2019-12-27T18:11:19.117Z"),
"before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "filter": {"foo": "bar"},
"max_depth": 0,
"url": "url",
"weight": 0,
@@ -329,6 +340,7 @@ def test_method_upload_with_all_params(self, client: Hyperspell) -> None:
memory = client.memories.upload(
file=b"raw file contents",
collection="collection",
+ metadata="metadata",
)
assert_matches_type(MemoryStatus, memory, path=["response"])
@@ -452,6 +464,7 @@ async def test_method_add_with_all_params(self, async_client: AsyncHyperspell) -
text="text",
collection="collection",
date=parse_datetime("2019-12-27T18:11:19.117Z"),
+ metadata={"foo": "string"},
resource_id="resource_id",
title="title",
)
@@ -543,27 +556,33 @@ async def test_method_search_with_all_params(self, async_client: AsyncHyperspell
"box": {
"after": parse_datetime("2019-12-27T18:11:19.117Z"),
"before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "filter": {"foo": "bar"},
"weight": 0,
},
"collections": {
"after": parse_datetime("2019-12-27T18:11:19.117Z"),
"before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "filter": {"foo": "bar"},
"weight": 0,
},
+ "filter": {"foo": "bar"},
"google_calendar": {
"after": parse_datetime("2019-12-27T18:11:19.117Z"),
"before": parse_datetime("2019-12-27T18:11:19.117Z"),
"calendar_id": "calendar_id",
+ "filter": {"foo": "bar"},
"weight": 0,
},
"google_drive": {
"after": parse_datetime("2019-12-27T18:11:19.117Z"),
"before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "filter": {"foo": "bar"},
"weight": 0,
},
"google_mail": {
"after": parse_datetime("2019-12-27T18:11:19.117Z"),
"before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "filter": {"foo": "bar"},
"label_ids": ["string"],
"weight": 0,
},
@@ -571,12 +590,14 @@ async def test_method_search_with_all_params(self, async_client: AsyncHyperspell
"notion": {
"after": parse_datetime("2019-12-27T18:11:19.117Z"),
"before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "filter": {"foo": "bar"},
"notion_page_ids": ["string"],
"weight": 0,
},
"reddit": {
"after": parse_datetime("2019-12-27T18:11:19.117Z"),
"before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "filter": {"foo": "bar"},
"period": "hour",
"sort": "relevance",
"subreddit": "subreddit",
@@ -587,6 +608,7 @@ async def test_method_search_with_all_params(self, async_client: AsyncHyperspell
"before": parse_datetime("2019-12-27T18:11:19.117Z"),
"channels": ["string"],
"exclude_archived": True,
+ "filter": {"foo": "bar"},
"include_dms": True,
"include_group_dms": True,
"include_private": True,
@@ -595,6 +617,7 @@ async def test_method_search_with_all_params(self, async_client: AsyncHyperspell
"web_crawler": {
"after": parse_datetime("2019-12-27T18:11:19.117Z"),
"before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "filter": {"foo": "bar"},
"max_depth": 0,
"url": "url",
"weight": 0,
@@ -665,6 +688,7 @@ async def test_method_upload_with_all_params(self, async_client: AsyncHyperspell
memory = await async_client.memories.upload(
file=b"raw file contents",
collection="collection",
+ metadata="metadata",
)
assert_matches_type(MemoryStatus, memory, path=["response"])
From 4eff9eb73c153fa2062d98c363815a2c82321ba5 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 12 Dec 2025 05:38:07 +0000
Subject: [PATCH 25/28] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index 1bb075d8..7ebd6f7f 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 21
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-622b43986c45c1efbeb06dd933786980257f300b7a0edbb2d2a4f708afacce36.yml
openapi_spec_hash: ade837ffc4873d3b50a0fab3f061b397
-config_hash: ca45358b5407440488ec988e3ee21412
+config_hash: 0ea77dd7e621e2fddf332beed1c5f162
From b005609bbfd778bf2bbb8eba1c22fae89f75441a Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 12 Dec 2025 05:42:10 +0000
Subject: [PATCH 26/28] feat(api): update via SDK Studio
---
.stats.yml | 2 +-
api.md | 12 +-
src/hyperspell/resources/memories.py | 23 ++--
src/hyperspell/types/__init__.py | 3 +-
.../{memory_get_response.py => memory.py} | 4 +-
src/hyperspell/types/memory_list_response.py | 105 ------------------
src/hyperspell/types/shared/query_result.py | 105 +-----------------
tests/api_resources/test_memories.py | 31 +++---
8 files changed, 37 insertions(+), 248 deletions(-)
rename src/hyperspell/types/{memory_get_response.py => memory.py} (96%)
delete mode 100644 src/hyperspell/types/memory_list_response.py
diff --git a/.stats.yml b/.stats.yml
index 7ebd6f7f..910cafe0 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 21
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-622b43986c45c1efbeb06dd933786980257f300b7a0edbb2d2a4f708afacce36.yml
openapi_spec_hash: ade837ffc4873d3b50a0fab3f061b397
-config_hash: 0ea77dd7e621e2fddf332beed1c5f162
+config_hash: e67e5a4ecb8ef7045e34efec09ee5e6c
diff --git a/api.md b/api.md
index edab7bca..0452defa 100644
--- a/api.md
+++ b/api.md
@@ -65,21 +65,15 @@ Methods:
Types:
```python
-from hyperspell.types import (
- MemoryStatus,
- MemoryListResponse,
- MemoryDeleteResponse,
- MemoryGetResponse,
- MemoryStatusResponse,
-)
+from hyperspell.types import Memory, MemoryStatus, MemoryDeleteResponse, MemoryStatusResponse
```
Methods:
-- client.memories.list(\*\*params) -> SyncCursorPage[MemoryListResponse]
+- client.memories.list(\*\*params) -> SyncCursorPage[Memory]
- client.memories.delete(resource_id, \*, source) -> MemoryDeleteResponse
- client.memories.add(\*\*params) -> MemoryStatus
-- client.memories.get(resource_id, \*, source) -> MemoryGetResponse
+- client.memories.get(resource_id, \*, source) -> Memory
- client.memories.search(\*\*params) -> QueryResult
- client.memories.status() -> MemoryStatusResponse
- client.memories.upload(\*\*params) -> MemoryStatus
diff --git a/src/hyperspell/resources/memories.py b/src/hyperspell/resources/memories.py
index dd4c48a7..628e868b 100644
--- a/src/hyperspell/resources/memories.py
+++ b/src/hyperspell/resources/memories.py
@@ -21,10 +21,9 @@
)
from ..pagination import SyncCursorPage, AsyncCursorPage
from .._base_client import AsyncPaginator, make_request_options
+from ..types.memory import Memory
from ..types.memory_status import MemoryStatus
-from ..types.memory_get_response import MemoryGetResponse
from ..types.shared.query_result import QueryResult
-from ..types.memory_list_response import MemoryListResponse
from ..types.memory_delete_response import MemoryDeleteResponse
from ..types.memory_status_response import MemoryStatusResponse
@@ -114,7 +113,7 @@ def list(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> SyncCursorPage[MemoryListResponse]:
+ ) -> SyncCursorPage[Memory]:
"""This endpoint allows you to paginate through all documents in the index.
You can
@@ -135,7 +134,7 @@ def list(
"""
return self._get_api_list(
"/memories/list",
- page=SyncCursorPage[MemoryListResponse],
+ page=SyncCursorPage[Memory],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -151,7 +150,7 @@ def list(
memory_list_params.MemoryListParams,
),
),
- model=MemoryListResponse,
+ model=Memory,
)
def delete(
@@ -376,7 +375,7 @@ def get(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> MemoryGetResponse:
+ ) -> Memory:
"""
Retrieves a document by provider and resource_id.
@@ -398,7 +397,7 @@ def get(
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=MemoryGetResponse,
+ cast_to=Memory,
)
def search(
@@ -670,7 +669,7 @@ def list(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> AsyncPaginator[MemoryListResponse, AsyncCursorPage[MemoryListResponse]]:
+ ) -> AsyncPaginator[Memory, AsyncCursorPage[Memory]]:
"""This endpoint allows you to paginate through all documents in the index.
You can
@@ -691,7 +690,7 @@ def list(
"""
return self._get_api_list(
"/memories/list",
- page=AsyncCursorPage[MemoryListResponse],
+ page=AsyncCursorPage[Memory],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -707,7 +706,7 @@ def list(
memory_list_params.MemoryListParams,
),
),
- model=MemoryListResponse,
+ model=Memory,
)
async def delete(
@@ -932,7 +931,7 @@ async def get(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> MemoryGetResponse:
+ ) -> Memory:
"""
Retrieves a document by provider and resource_id.
@@ -954,7 +953,7 @@ async def get(
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=MemoryGetResponse,
+ cast_to=Memory,
)
async def search(
diff --git a/src/hyperspell/types/__init__.py b/src/hyperspell/types/__init__.py
index 2ba5f9b2..fb573526 100644
--- a/src/hyperspell/types/__init__.py
+++ b/src/hyperspell/types/__init__.py
@@ -3,15 +3,14 @@
from __future__ import annotations
from .token import Token as Token
+from .memory import Memory as Memory
from .shared import QueryResult as QueryResult
from .memory_status import MemoryStatus as MemoryStatus
from .auth_me_response import AuthMeResponse as AuthMeResponse
from .memory_add_params import MemoryAddParams as MemoryAddParams
from .vault_list_params import VaultListParams as VaultListParams
from .memory_list_params import MemoryListParams as MemoryListParams
-from .memory_get_response import MemoryGetResponse as MemoryGetResponse
from .vault_list_response import VaultListResponse as VaultListResponse
-from .memory_list_response import MemoryListResponse as MemoryListResponse
from .memory_search_params import MemorySearchParams as MemorySearchParams
from .memory_upload_params import MemoryUploadParams as MemoryUploadParams
from .auth_user_token_params import AuthUserTokenParams as AuthUserTokenParams
diff --git a/src/hyperspell/types/memory_get_response.py b/src/hyperspell/types/memory.py
similarity index 96%
rename from src/hyperspell/types/memory_get_response.py
rename to src/hyperspell/types/memory.py
index 093ce1b9..df0de529 100644
--- a/src/hyperspell/types/memory_get_response.py
+++ b/src/hyperspell/types/memory.py
@@ -8,7 +8,7 @@
from .._models import BaseModel
-__all__ = ["MemoryGetResponse", "Metadata", "MetadataEvent"]
+__all__ = ["Memory", "Metadata", "MetadataEvent"]
class MetadataEvent(BaseModel):
@@ -45,7 +45,7 @@ def __getattr__(self, attr: str) -> object: ...
__pydantic_extra__: Dict[str, object]
-class MemoryGetResponse(BaseModel):
+class Memory(BaseModel):
resource_id: str
source: Literal[
diff --git a/src/hyperspell/types/memory_list_response.py b/src/hyperspell/types/memory_list_response.py
deleted file mode 100644
index 692ca8e5..00000000
--- a/src/hyperspell/types/memory_list_response.py
+++ /dev/null
@@ -1,105 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from typing import TYPE_CHECKING, Dict, List, Optional
-from datetime import datetime
-from typing_extensions import Literal
-
-from pydantic import Field as FieldInfo
-
-from .._models import BaseModel
-
-__all__ = ["MemoryListResponse", "Metadata", "MetadataEvent"]
-
-
-class MetadataEvent(BaseModel):
- message: str
-
- type: Literal["error", "warning", "info", "success"]
-
- time: Optional[datetime] = None
-
-
-class Metadata(BaseModel):
- created_at: Optional[datetime] = None
-
- events: Optional[List[MetadataEvent]] = None
-
- indexed_at: Optional[datetime] = None
-
- last_modified: Optional[datetime] = None
-
- status: Optional[Literal["pending", "processing", "completed", "failed"]] = None
-
- url: Optional[str] = None
-
- if TYPE_CHECKING:
- # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a
- # value to this field, so for compatibility we avoid doing it at runtime.
- __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride]
-
- # Stub to indicate that arbitrary properties are accepted.
- # To access properties that are not valid identifiers you can use `getattr`, e.g.
- # `getattr(obj, '$type')`
- def __getattr__(self, attr: str) -> object: ...
- else:
- __pydantic_extra__: Dict[str, object]
-
-
-class MemoryListResponse(BaseModel):
- resource_id: str
-
- source: Literal[
- "collections",
- "vault",
- "web_crawler",
- "notion",
- "slack",
- "google_calendar",
- "reddit",
- "box",
- "google_drive",
- "airtable",
- "algolia",
- "amplitude",
- "asana",
- "ashby",
- "bamboohr",
- "basecamp",
- "bubbles",
- "calendly",
- "confluence",
- "clickup",
- "datadog",
- "deel",
- "discord",
- "dropbox",
- "exa",
- "facebook",
- "front",
- "github",
- "gitlab",
- "google_docs",
- "google_mail",
- "google_sheet",
- "hubspot",
- "jira",
- "linear",
- "microsoft_teams",
- "mixpanel",
- "monday",
- "outlook",
- "perplexity",
- "rippling",
- "salesforce",
- "segment",
- "todoist",
- "twitter",
- "zoom",
- ]
-
- metadata: Optional[Metadata] = None
-
- score: Optional[float] = None
- """The relevance of the resource to the query"""
-
- title: Optional[str] = None
diff --git a/src/hyperspell/types/shared/query_result.py b/src/hyperspell/types/shared/query_result.py
index b2c02f9c..67fdb09c 100644
--- a/src/hyperspell/types/shared/query_result.py
+++ b/src/hyperspell/types/shared/query_result.py
@@ -1,112 +1,15 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import TYPE_CHECKING, Dict, List, Optional
-from datetime import datetime
-from typing_extensions import Literal
-
-from pydantic import Field as FieldInfo
+from typing import Dict, List, Optional
+from ..memory import Memory
from ..._models import BaseModel
-__all__ = ["QueryResult", "Document", "DocumentMetadata", "DocumentMetadataEvent"]
-
-
-class DocumentMetadataEvent(BaseModel):
- message: str
-
- type: Literal["error", "warning", "info", "success"]
-
- time: Optional[datetime] = None
-
-
-class DocumentMetadata(BaseModel):
- created_at: Optional[datetime] = None
-
- events: Optional[List[DocumentMetadataEvent]] = None
-
- indexed_at: Optional[datetime] = None
-
- last_modified: Optional[datetime] = None
-
- status: Optional[Literal["pending", "processing", "completed", "failed"]] = None
-
- url: Optional[str] = None
-
- if TYPE_CHECKING:
- # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a
- # value to this field, so for compatibility we avoid doing it at runtime.
- __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride]
-
- # Stub to indicate that arbitrary properties are accepted.
- # To access properties that are not valid identifiers you can use `getattr`, e.g.
- # `getattr(obj, '$type')`
- def __getattr__(self, attr: str) -> object: ...
- else:
- __pydantic_extra__: Dict[str, object]
-
-
-class Document(BaseModel):
- resource_id: str
-
- source: Literal[
- "collections",
- "vault",
- "web_crawler",
- "notion",
- "slack",
- "google_calendar",
- "reddit",
- "box",
- "google_drive",
- "airtable",
- "algolia",
- "amplitude",
- "asana",
- "ashby",
- "bamboohr",
- "basecamp",
- "bubbles",
- "calendly",
- "confluence",
- "clickup",
- "datadog",
- "deel",
- "discord",
- "dropbox",
- "exa",
- "facebook",
- "front",
- "github",
- "gitlab",
- "google_docs",
- "google_mail",
- "google_sheet",
- "hubspot",
- "jira",
- "linear",
- "microsoft_teams",
- "mixpanel",
- "monday",
- "outlook",
- "perplexity",
- "rippling",
- "salesforce",
- "segment",
- "todoist",
- "twitter",
- "zoom",
- ]
-
- metadata: Optional[DocumentMetadata] = None
-
- score: Optional[float] = None
- """The relevance of the resource to the query"""
-
- title: Optional[str] = None
+__all__ = ["QueryResult"]
class QueryResult(BaseModel):
- documents: List[Document]
+ documents: List[Memory]
answer: Optional[str] = None
"""The answer to the query, if the request was set to answer."""
diff --git a/tests/api_resources/test_memories.py b/tests/api_resources/test_memories.py
index 1dc9b321..87f5dbac 100644
--- a/tests/api_resources/test_memories.py
+++ b/tests/api_resources/test_memories.py
@@ -10,9 +10,8 @@
from hyperspell import Hyperspell, AsyncHyperspell
from tests.utils import assert_matches_type
from hyperspell.types import (
+ Memory,
MemoryStatus,
- MemoryGetResponse,
- MemoryListResponse,
MemoryDeleteResponse,
MemoryStatusResponse,
)
@@ -29,7 +28,7 @@ class TestMemories:
@parametrize
def test_method_list(self, client: Hyperspell) -> None:
memory = client.memories.list()
- assert_matches_type(SyncCursorPage[MemoryListResponse], memory, path=["response"])
+ assert_matches_type(SyncCursorPage[Memory], memory, path=["response"])
@parametrize
def test_method_list_with_all_params(self, client: Hyperspell) -> None:
@@ -39,7 +38,7 @@ def test_method_list_with_all_params(self, client: Hyperspell) -> None:
size=0,
source="collections",
)
- assert_matches_type(SyncCursorPage[MemoryListResponse], memory, path=["response"])
+ assert_matches_type(SyncCursorPage[Memory], memory, path=["response"])
@parametrize
def test_raw_response_list(self, client: Hyperspell) -> None:
@@ -48,7 +47,7 @@ def test_raw_response_list(self, client: Hyperspell) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = response.parse()
- assert_matches_type(SyncCursorPage[MemoryListResponse], memory, path=["response"])
+ assert_matches_type(SyncCursorPage[Memory], memory, path=["response"])
@parametrize
def test_streaming_response_list(self, client: Hyperspell) -> None:
@@ -57,7 +56,7 @@ def test_streaming_response_list(self, client: Hyperspell) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = response.parse()
- assert_matches_type(SyncCursorPage[MemoryListResponse], memory, path=["response"])
+ assert_matches_type(SyncCursorPage[Memory], memory, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -152,7 +151,7 @@ def test_method_get(self, client: Hyperspell) -> None:
resource_id="resource_id",
source="collections",
)
- assert_matches_type(MemoryGetResponse, memory, path=["response"])
+ assert_matches_type(Memory, memory, path=["response"])
@parametrize
def test_raw_response_get(self, client: Hyperspell) -> None:
@@ -164,7 +163,7 @@ def test_raw_response_get(self, client: Hyperspell) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = response.parse()
- assert_matches_type(MemoryGetResponse, memory, path=["response"])
+ assert_matches_type(Memory, memory, path=["response"])
@parametrize
def test_streaming_response_get(self, client: Hyperspell) -> None:
@@ -176,7 +175,7 @@ def test_streaming_response_get(self, client: Hyperspell) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = response.parse()
- assert_matches_type(MemoryGetResponse, memory, path=["response"])
+ assert_matches_type(Memory, memory, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -377,7 +376,7 @@ class TestAsyncMemories:
@parametrize
async def test_method_list(self, async_client: AsyncHyperspell) -> None:
memory = await async_client.memories.list()
- assert_matches_type(AsyncCursorPage[MemoryListResponse], memory, path=["response"])
+ assert_matches_type(AsyncCursorPage[Memory], memory, path=["response"])
@parametrize
async def test_method_list_with_all_params(self, async_client: AsyncHyperspell) -> None:
@@ -387,7 +386,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncHyperspell)
size=0,
source="collections",
)
- assert_matches_type(AsyncCursorPage[MemoryListResponse], memory, path=["response"])
+ assert_matches_type(AsyncCursorPage[Memory], memory, path=["response"])
@parametrize
async def test_raw_response_list(self, async_client: AsyncHyperspell) -> None:
@@ -396,7 +395,7 @@ async def test_raw_response_list(self, async_client: AsyncHyperspell) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = await response.parse()
- assert_matches_type(AsyncCursorPage[MemoryListResponse], memory, path=["response"])
+ assert_matches_type(AsyncCursorPage[Memory], memory, path=["response"])
@parametrize
async def test_streaming_response_list(self, async_client: AsyncHyperspell) -> None:
@@ -405,7 +404,7 @@ async def test_streaming_response_list(self, async_client: AsyncHyperspell) -> N
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = await response.parse()
- assert_matches_type(AsyncCursorPage[MemoryListResponse], memory, path=["response"])
+ assert_matches_type(AsyncCursorPage[Memory], memory, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -500,7 +499,7 @@ async def test_method_get(self, async_client: AsyncHyperspell) -> None:
resource_id="resource_id",
source="collections",
)
- assert_matches_type(MemoryGetResponse, memory, path=["response"])
+ assert_matches_type(Memory, memory, path=["response"])
@parametrize
async def test_raw_response_get(self, async_client: AsyncHyperspell) -> None:
@@ -512,7 +511,7 @@ async def test_raw_response_get(self, async_client: AsyncHyperspell) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = await response.parse()
- assert_matches_type(MemoryGetResponse, memory, path=["response"])
+ assert_matches_type(Memory, memory, path=["response"])
@parametrize
async def test_streaming_response_get(self, async_client: AsyncHyperspell) -> None:
@@ -524,7 +523,7 @@ async def test_streaming_response_get(self, async_client: AsyncHyperspell) -> No
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = await response.parse()
- assert_matches_type(MemoryGetResponse, memory, path=["response"])
+ assert_matches_type(Memory, memory, path=["response"])
assert cast(Any, response.is_closed) is True
From 18945ea3354e140c6d150fb0d8e2238a33113c5e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 12 Dec 2025 05:43:18 +0000
Subject: [PATCH 27/28] feat(api): update via SDK Studio
---
.stats.yml | 2 +-
README.md | 8 ++++----
src/hyperspell/_client.py | 12 ++++++------
tests/test_client.py | 4 ++--
4 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 910cafe0..87168119 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 21
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-622b43986c45c1efbeb06dd933786980257f300b7a0edbb2d2a4f708afacce36.yml
openapi_spec_hash: ade837ffc4873d3b50a0fab3f061b397
-config_hash: e67e5a4ecb8ef7045e34efec09ee5e6c
+config_hash: a3a8e3c71c17eabb21ab8173521181a4
diff --git a/README.md b/README.md
index ef641e61..65ced851 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ import os
from hyperspell import Hyperspell
client = Hyperspell(
- api_key=os.environ.get("HYPERSPELL_TOKEN"), # This is the default and can be omitted
+ api_key=os.environ.get("HYPERSPELL_API_KEY"), # This is the default and can be omitted
)
memory_status = client.memories.add(
@@ -40,7 +40,7 @@ print(memory_status.resource_id)
While you can provide an `api_key` keyword argument,
we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/)
-to add `HYPERSPELL_TOKEN="My API Key"` to your `.env` file
+to add `HYPERSPELL_API_KEY="My API Key"` to your `.env` file
so that your API Key is not stored in source control.
## Async usage
@@ -53,7 +53,7 @@ import asyncio
from hyperspell import AsyncHyperspell
client = AsyncHyperspell(
- api_key=os.environ.get("HYPERSPELL_TOKEN"), # This is the default and can be omitted
+ api_key=os.environ.get("HYPERSPELL_API_KEY"), # This is the default and can be omitted
)
@@ -91,7 +91,7 @@ from hyperspell import AsyncHyperspell
async def main() -> None:
async with AsyncHyperspell(
- api_key=os.environ.get("HYPERSPELL_TOKEN"), # This is the default and can be omitted
+ api_key=os.environ.get("HYPERSPELL_API_KEY"), # This is the default and can be omitted
http_client=DefaultAioHttpClient(),
) as client:
memory_status = await client.memories.add(
diff --git a/src/hyperspell/_client.py b/src/hyperspell/_client.py
index a6aa68c4..a41a2c80 100644
--- a/src/hyperspell/_client.py
+++ b/src/hyperspell/_client.py
@@ -83,13 +83,13 @@ def __init__(
) -> None:
"""Construct a new synchronous Hyperspell client instance.
- This automatically infers the `api_key` argument from the `HYPERSPELL_TOKEN` environment variable if it is not provided.
+ This automatically infers the `api_key` argument from the `HYPERSPELL_API_KEY` environment variable if it is not provided.
"""
if api_key is None:
- api_key = os.environ.get("HYPERSPELL_TOKEN")
+ api_key = os.environ.get("HYPERSPELL_API_KEY")
if api_key is None:
raise HyperspellError(
- "The api_key client option must be set either by passing api_key to the client or by setting the HYPERSPELL_TOKEN environment variable"
+ "The api_key client option must be set either by passing api_key to the client or by setting the HYPERSPELL_API_KEY environment variable"
)
self.api_key = api_key
@@ -278,13 +278,13 @@ def __init__(
) -> None:
"""Construct a new async AsyncHyperspell client instance.
- This automatically infers the `api_key` argument from the `HYPERSPELL_TOKEN` environment variable if it is not provided.
+ This automatically infers the `api_key` argument from the `HYPERSPELL_API_KEY` environment variable if it is not provided.
"""
if api_key is None:
- api_key = os.environ.get("HYPERSPELL_TOKEN")
+ api_key = os.environ.get("HYPERSPELL_API_KEY")
if api_key is None:
raise HyperspellError(
- "The api_key client option must be set either by passing api_key to the client or by setting the HYPERSPELL_TOKEN environment variable"
+ "The api_key client option must be set either by passing api_key to the client or by setting the HYPERSPELL_API_KEY environment variable"
)
self.api_key = api_key
diff --git a/tests/test_client.py b/tests/test_client.py
index 21615814..770b4bba 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -389,7 +389,7 @@ def test_validate_headers(self) -> None:
assert request.headers.get("X-As-User") == user_id
with pytest.raises(HyperspellError):
- with update_env(**{"HYPERSPELL_TOKEN": Omit()}):
+ with update_env(**{"HYPERSPELL_API_KEY": Omit()}):
client2 = Hyperspell(base_url=base_url, api_key=None, user_id=None, _strict_response_validation=True)
_ = client2
@@ -1278,7 +1278,7 @@ def test_validate_headers(self) -> None:
assert request.headers.get("X-As-User") == user_id
with pytest.raises(HyperspellError):
- with update_env(**{"HYPERSPELL_TOKEN": Omit()}):
+ with update_env(**{"HYPERSPELL_API_KEY": Omit()}):
client2 = AsyncHyperspell(
base_url=base_url, api_key=None, user_id=None, _strict_response_validation=True
)
From 25b4167bd9e4e89efc0723a654acfdd3fe7c62dc Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 12 Dec 2025 05:43:44 +0000
Subject: [PATCH 28/28] release: 0.27.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 37 +++++++++++++++++++++++++++++++++++
pyproject.toml | 2 +-
src/hyperspell/_version.py | 2 +-
4 files changed, 40 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index caf5ca3f..59acac47 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.26.0"
+ ".": "0.27.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ea68271c..77dc76c7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,42 @@
# Changelog
+## 0.27.0 (2025-12-12)
+
+Full Changelog: [v0.26.0...v0.27.0](https://github.com/hyperspell/python-sdk/compare/v0.26.0...v0.27.0)
+
+### Features
+
+* **api:** api update ([cc4b7a1](https://github.com/hyperspell/python-sdk/commit/cc4b7a13f9aa8f38e2d115fc2292cf6b1f959cd0))
+* **api:** api update ([2e9f356](https://github.com/hyperspell/python-sdk/commit/2e9f356bd097204a9d89b927768e261e79397b36))
+* **api:** api update ([0f29501](https://github.com/hyperspell/python-sdk/commit/0f2950160b0842b7522f9a7ac3100b003f276cc4))
+* **api:** api update ([add1d76](https://github.com/hyperspell/python-sdk/commit/add1d76538a307e7a1c3fc592207d50351c4c5ca))
+* **api:** api update ([1bfef28](https://github.com/hyperspell/python-sdk/commit/1bfef28fdcd09c6b6cccfb0aa21055b2ad220403))
+* **api:** update via SDK Studio ([18945ea](https://github.com/hyperspell/python-sdk/commit/18945ea3354e140c6d150fb0d8e2238a33113c5e))
+* **api:** update via SDK Studio ([b005609](https://github.com/hyperspell/python-sdk/commit/b005609bbfd778bf2bbb8eba1c22fae89f75441a))
+* **api:** update via SDK Studio ([49e68c8](https://github.com/hyperspell/python-sdk/commit/49e68c866483f9be8900eef7fc3e776e43cd74b1))
+
+
+### Bug Fixes
+
+* **client:** close streams without requiring full consumption ([669e4cf](https://github.com/hyperspell/python-sdk/commit/669e4cf750158df6aa381083911c051a6afb8f07))
+* compat with Python 3.14 ([53fdd97](https://github.com/hyperspell/python-sdk/commit/53fdd97e8631bf9093c775de401b0f1fd8965efe))
+* **compat:** update signatures of `model_dump` and `model_dump_json` for Pydantic v1 ([6ed583e](https://github.com/hyperspell/python-sdk/commit/6ed583ec067269fb1c27c8dcce9167795a1abd17))
+* ensure streams are always closed ([c676043](https://github.com/hyperspell/python-sdk/commit/c6760434add9e200c865154a08d0289d556b70a8))
+* **types:** allow pyright to infer TypedDict types within SequenceNotStr ([9c7824f](https://github.com/hyperspell/python-sdk/commit/9c7824f603cd5b1935ee1aac676e3fcf299d51f6))
+
+
+### Chores
+
+* add missing docstrings ([16f09b6](https://github.com/hyperspell/python-sdk/commit/16f09b68057a3be4b878f32980d7d07fefbc7cb9))
+* add Python 3.14 classifier and testing ([75b429e](https://github.com/hyperspell/python-sdk/commit/75b429e1dc942a6595024682d7bf9d7b22cad632))
+* bump `httpx-aiohttp` version to 0.1.9 ([802a6ee](https://github.com/hyperspell/python-sdk/commit/802a6ee6c0b67bddd725727078fc9c3ce2650b69))
+* **deps:** mypy 1.18.1 has a regression, pin to 1.17 ([223b7fc](https://github.com/hyperspell/python-sdk/commit/223b7fcd14499af7a908d7ddb9cbe83e44656737))
+* **docs:** use environment variables for authentication in code snippets ([0e42e5d](https://github.com/hyperspell/python-sdk/commit/0e42e5d97de61b3224eeaac980df5efc35a9336e))
+* **internal/tests:** avoid race condition with implicit client cleanup ([01d946a](https://github.com/hyperspell/python-sdk/commit/01d946a29b5dbfd37f2f7a7c93f6824c5865af1b))
+* **internal:** grammar fix (it's -> its) ([516dd5e](https://github.com/hyperspell/python-sdk/commit/516dd5e4f13fcffa9593e58f7a4c8bf855968ae0))
+* **package:** drop Python 3.8 support ([4d8d0a5](https://github.com/hyperspell/python-sdk/commit/4d8d0a501af22b0d5321e34e1d6c368dec622d34))
+* update lockfile ([9170730](https://github.com/hyperspell/python-sdk/commit/9170730ae0dd8259dcd9b5814b76e6a79247b237))
+
## 0.26.0 (2025-10-14)
Full Changelog: [v0.25.0...v0.26.0](https://github.com/hyperspell/python-sdk/compare/v0.25.0...v0.26.0)
diff --git a/pyproject.toml b/pyproject.toml
index 0853d986..1dd7a71c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "hyperspell"
-version = "0.26.0"
+version = "0.27.0"
description = "The official Python library for the hyperspell API"
dynamic = ["readme"]
license = "MIT"
diff --git a/src/hyperspell/_version.py b/src/hyperspell/_version.py
index d4a6d30b..5d030cd7 100644
--- a/src/hyperspell/_version.py
+++ b/src/hyperspell/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "hyperspell"
-__version__ = "0.26.0" # x-release-please-version
+__version__ = "0.27.0" # x-release-please-version