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/.stats.yml b/.stats.yml
index 06232146..87168119 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
-config_hash: e29127278ff246754ce4801403db0cd9
+configured_endpoints: 21
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-622b43986c45c1efbeb06dd933786980257f300b7a0edbb2d2a4f708afacce36.yml
+openapi_spec_hash: ade837ffc4873d3b50a0fab3f061b397
+config_hash: a3a8e3c71c17eabb21ab8173521181a4
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/README.md b/README.md
index c94d2be3..65ced851 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).
@@ -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
)
@@ -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_API_KEY"), # This is the default and can be omitted
http_client=DefaultAioHttpClient(),
) as client:
memory_status = await client.memories.add(
@@ -476,7 +477,7 @@ print(hyperspell.__version__)
## Requirements
-Python 3.8 or higher.
+Python 3.9 or higher.
## Contributing
diff --git a/api.md b/api.md
index a0af7c08..0452defa 100644
--- a/api.md
+++ b/api.md
@@ -4,23 +4,31 @@
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:
```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/pyproject.toml b/pyproject.toml
index 1889a095..1dd7a71c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,30 +1,32 @@
[project]
name = "hyperspell"
-version = "0.26.0"
+version = "0.27.0"
description = "The official Python library for the hyperspell API"
dynamic = ["readme"]
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.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",
"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",
@@ -39,14 +41,14 @@ 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
# version pins are in requirements-dev.lock
dev-dependencies = [
"pyright==1.1.399",
- "mypy",
+ "mypy==1.17",
"respx",
"pytest",
"pytest-asyncio",
@@ -141,7 +143,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/requirements-dev.lock b/requirements-dev.lock
index c3a47ff0..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
@@ -56,82 +61,89 @@ 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
+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.14.1
-mypy-extensions==1.0.0
+mypy==1.17.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
-platformdirs==3.11.0
+pathspec==0.12.1
+ # via mypy
+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 e90ee670..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
@@ -43,33 +43,34 @@ 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
+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.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
+sniffio==1.3.1
# via hyperspell
-typing-extensions==4.12.2
+typing-extensions==4.15.0
+ # via aiosignal
# via anyio
+ # via exceptiongroup
# 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
+yarl==1.22.0
# via aiohttp
diff --git a/src/hyperspell/_client.py b/src/hyperspell/_client.py
index 3abe0ae8..a41a2c80 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
@@ -82,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
@@ -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
@@ -275,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
@@ -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/_models.py b/src/hyperspell/_models.py
index 6a3cd1d2..ca9500b2 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 (
@@ -256,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
@@ -272,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.
@@ -298,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,
@@ -314,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:
@@ -354,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,
@@ -573,6 +591,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 +636,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 +691,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/src/hyperspell/_streaming.py b/src/hyperspell/_streaming.py
index 327dec92..11fc4ffe 100644
--- a/src/hyperspell/_streaming.py
+++ b/src/hyperspell/_streaming.py
@@ -54,12 +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)
-
- # Ensure the entire stream is consumed
- for _sse in iterator:
- ...
+ 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
@@ -118,12 +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)
-
- # Ensure the entire stream is consumed
- async for _sse in iterator:
- ...
+ 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
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
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:
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
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
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/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/resources/memories.py b/src/hyperspell/resources/memories.py
index 044081ad..628e868b 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
@@ -256,6 +256,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.
@@ -281,6 +282,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.
@@ -301,6 +305,7 @@ def add(
"text": text,
"collection": collection,
"date": date,
+ "metadata": metadata,
"resource_id": resource_id,
"title": title,
},
@@ -527,6 +532,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,
@@ -546,6 +552,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
@@ -558,6 +567,7 @@ def upload(
{
"file": file,
"collection": collection,
+ "metadata": metadata,
}
)
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
@@ -802,6 +812,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.
@@ -827,6 +838,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.
@@ -847,6 +861,7 @@ async def add(
"text": text,
"collection": collection,
"date": date,
+ "metadata": metadata,
"resource_id": resource_id,
"title": title,
},
@@ -1073,6 +1088,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,
@@ -1092,6 +1108,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
@@ -1104,6 +1123,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/__init__.py b/src/hyperspell/types/__init__.py
index 5f3077cf..fb573526 100644
--- a/src/hyperspell/types/__init__.py
+++ b/src/hyperspell/types/__init__.py
@@ -16,11 +16,12 @@
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_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..7eee2ac6 100644
--- a/src/hyperspell/types/auth_me_response.py
+++ b/src/hyperspell/types/auth_me_response.py
@@ -6,10 +6,12 @@
from .._models import BaseModel
-__all__ = ["AuthMeResponse", "App", "Connection"]
+__all__ = ["AuthMeResponse", "App"]
class App(BaseModel):
+ """The Hyperspell app's id this user belongs to"""
+
id: str
"""The Hyperspell app's id this user belongs to"""
@@ -23,64 +25,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 +84,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/connection_list_response.py b/src/hyperspell/types/connection_list_response.py
new file mode 100644
index 00000000..0a60e0e6
--- /dev/null
+++ b/src/hyperspell/types/connection_list_response.py
@@ -0,0 +1,73 @@
+# 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"""
+
+ integration_id: str
+ """The connection's integration 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/integration_revoke_response.py b/src/hyperspell/types/connection_revoke_response.py
similarity index 65%
rename from src/hyperspell/types/integration_revoke_response.py
rename to src/hyperspell/types/connection_revoke_response.py
index 2385ca96..63530302 100644
--- a/src/hyperspell/types/integration_revoke_response.py
+++ b/src/hyperspell/types/connection_revoke_response.py
@@ -2,10 +2,10 @@
from .._models import BaseModel
-__all__ = ["IntegrationRevokeResponse"]
+__all__ = ["ConnectionRevokeResponse"]
-class IntegrationRevokeResponse(BaseModel):
+class ConnectionRevokeResponse(BaseModel):
message: str
success: bool
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",
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 07e692a2..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
@@ -91,12 +91,20 @@ 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."""
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.
@@ -107,12 +115,20 @@ 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."""
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.
@@ -123,6 +139,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."""
@@ -136,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.
@@ -146,12 +170,20 @@ 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."""
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.
@@ -162,12 +194,20 @@ 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."""
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']).
@@ -186,12 +226,20 @@ 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."""
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.
@@ -208,12 +256,20 @@ 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."""
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'."""
@@ -236,6 +292,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."""
@@ -248,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."""
@@ -270,16 +334,24 @@ 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."""
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"""
- url: Union[str, object]
+ url: Optional[str]
"""The URL to crawl"""
weight: float
@@ -292,6 +364,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."""
@@ -307,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_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(
+ "",
+ )
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(
- "",
- )
diff --git a/tests/api_resources/test_memories.py b/tests/api_resources/test_memories.py
index 182601a5..87f5dbac 100644
--- a/tests/api_resources/test_memories.py
+++ b/tests/api_resources/test_memories.py
@@ -115,6 +115,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",
)
@@ -206,27 +207,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,
},
@@ -234,12 +241,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",
@@ -250,6 +259,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,
@@ -258,8 +268,9 @@ 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": "string",
+ "url": "url",
"weight": 0,
},
},
@@ -328,6 +339,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"])
@@ -451,6 +463,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",
)
@@ -542,27 +555,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,
},
@@ -570,12 +589,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",
@@ -586,6 +607,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,
@@ -594,8 +616,9 @@ 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": "string",
+ "url": "url",
"weight": 0,
},
},
@@ -664,6 +687,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"])
diff --git a/tests/test_client.py b/tests/test_client.py
index eff94130..770b4bba 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"))
@@ -379,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
@@ -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"))
@@ -1248,13 +1278,13 @@ 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
)
_ = 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
)
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")