diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7e334a01..938ce889 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -19,7 +19,7 @@ jobs:
runs-on: ${{ github.repository == 'stainless-sdks/hyperspell-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Install Rye
run: |
@@ -44,7 +44,7 @@ jobs:
id-token: write
runs-on: ${{ github.repository == 'stainless-sdks/hyperspell-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Install Rye
run: |
@@ -81,7 +81,7 @@ jobs:
runs-on: ${{ github.repository == 'stainless-sdks/hyperspell-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Install Rye
run: |
diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml
index 50e544ee..53338790 100644
--- a/.github/workflows/publish-pypi.yml
+++ b/.github/workflows/publish-pypi.yml
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Install Rye
run: |
diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml
index 9d43ec89..7f739e2a 100644
--- a/.github/workflows/release-doctor.yml
+++ b/.github/workflows/release-doctor.yml
@@ -12,7 +12,7 @@ jobs:
if: github.repository == 'hyperspell/python-sdk' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Check release environment
run: |
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 8935e932..554e34bb 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.28.0"
+ ".": "0.30.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index f9df407d..b7a86017 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 22
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-fc4ab722e6762cc69d533f57bea0d70b00e44a30c4ad8144e14ff70a1170ec7c.yml
-openapi_spec_hash: 2533ea676c195d5f7d30a67c201fd32d
-config_hash: 5cb785fcdf07e4053f36b434e1db2d8a
+configured_endpoints: 23
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-a73c73c4848db6cc0b836219f2ace7bc9f6b4611d36f9daa2158f8bd5a7d0864.yml
+openapi_spec_hash: 4ef2aeca3ffe2c6e6fbca0770a69c6fb
+config_hash: bd77d0b7029518c697756456d6854f07
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e0dab305..ebe56bb6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,35 @@
# Changelog
+## 0.30.0 (2026-01-17)
+
+Full Changelog: [v0.28.0...v0.30.0](https://github.com/hyperspell/python-sdk/compare/v0.28.0...v0.30.0)
+
+### Features
+
+* **api:** api update ([b701ec5](https://github.com/hyperspell/python-sdk/commit/b701ec51489e3100814dfd3295161cc04867e679))
+* **api:** api update ([8553859](https://github.com/hyperspell/python-sdk/commit/855385906fced24870ed2e7b24cffe0ad5856bbf))
+* **api:** manual updates ([a4d305c](https://github.com/hyperspell/python-sdk/commit/a4d305c10a9364f499b2cd9eaf0fc02bb7dcb136))
+* **client:** add support for binary request streaming ([578c776](https://github.com/hyperspell/python-sdk/commit/578c77664d05aa2ffa1c7d6cc3f7e3e3d5c77991))
+
+
+### Bug Fixes
+
+* use async_to_httpx_files in patch method ([4116107](https://github.com/hyperspell/python-sdk/commit/4116107e99dbe09a060f53ebfedcf1ff58715674))
+
+
+### Chores
+
+* **internal:** add `--fix` argument to lint script ([9d02a46](https://github.com/hyperspell/python-sdk/commit/9d02a464264ed187e3072c7b9337c14101b21945))
+* **internal:** add missing files argument to base client ([22826f9](https://github.com/hyperspell/python-sdk/commit/22826f97b3edf8910eda4be67aa1c2e5dfdf200b))
+* **internal:** codegen related update ([ddb8f8f](https://github.com/hyperspell/python-sdk/commit/ddb8f8f25eb0f015edcb2cbb833316555a68adbf))
+* **internal:** update `actions/checkout` version ([2453986](https://github.com/hyperspell/python-sdk/commit/2453986d89b02dd99528eb40676ef0a087391e29))
+* speedup initial import ([b39343e](https://github.com/hyperspell/python-sdk/commit/b39343e600358251c604ee2d0232b3af538172f5))
+
+
+### Documentation
+
+* prominently feature MCP server setup in root SDK readmes ([115424d](https://github.com/hyperspell/python-sdk/commit/115424d9ff7344bad70fd0a92657ae6f5fbfc647))
+
## 0.28.0 (2025-12-15)
Full Changelog: [v0.27.0...v0.28.0](https://github.com/hyperspell/python-sdk/compare/v0.27.0...v0.28.0)
diff --git a/LICENSE b/LICENSE
index 9239282a..2634ff3d 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright 2025 hyperspell
+Copyright 2026 hyperspell
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
diff --git a/README.md b/README.md
index 65ced851..6080c5e0 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,15 @@ and offers both synchronous and asynchronous clients powered by [httpx](https://
It is generated with [Stainless](https://www.stainless.com/).
+## MCP Server
+
+Use the Hyperspell MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application.
+
+[](https://cursor.com/en-US/install-mcp?name=hyperspell-mcp&config=eyJuYW1lIjoiaHlwZXJzcGVsbC1tY3AiLCJ0cmFuc3BvcnQiOiJzc2UiLCJ1cmwiOiJodHRwczovL2h5cGVyc3BlbGwuc3RsbWNwLmNvbS9zc2UifQ)
+[](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22hyperspell-mcp%22%2C%22type%22%3A%22sse%22%2C%22url%22%3A%22https%3A%2F%2Fhyperspell.stlmcp.com%2Fsse%22%7D)
+
+> Note: You may need to set environment variables in your MCP client.
+
## Documentation
The REST API documentation can be found on [docs.hyperspell.com](https://docs.hyperspell.com/). The full API of this library can be found in [api.md](api.md).
diff --git a/api.md b/api.md
index 8cfcaeda..079f9be6 100644
--- a/api.md
+++ b/api.md
@@ -65,7 +65,13 @@ Methods:
Types:
```python
-from hyperspell.types import Memory, MemoryStatus, MemoryDeleteResponse, MemoryStatusResponse
+from hyperspell.types import (
+ Memory,
+ MemoryStatus,
+ MemoryDeleteResponse,
+ MemoryAddBulkResponse,
+ MemoryStatusResponse,
+)
```
Methods:
@@ -74,6 +80,7 @@ Methods:
- client.memories.list(\*\*params) -> SyncCursorPage[Memory]
- client.memories.delete(resource_id, \*, source) -> MemoryDeleteResponse
- client.memories.add(\*\*params) -> MemoryStatus
+- client.memories.add_bulk(\*\*params) -> MemoryAddBulkResponse
- client.memories.get(resource_id, \*, source) -> Memory
- client.memories.search(\*\*params) -> QueryResult
- client.memories.status() -> MemoryStatusResponse
diff --git a/pyproject.toml b/pyproject.toml
index 87fff548..8f3ea649 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "hyperspell"
-version = "0.28.0"
+version = "0.30.0"
description = "The official Python library for the hyperspell API"
dynamic = ["readme"]
license = "MIT"
diff --git a/scripts/lint b/scripts/lint
index 6453a7bd..9d4a19f1 100755
--- a/scripts/lint
+++ b/scripts/lint
@@ -4,8 +4,13 @@ set -e
cd "$(dirname "$0")/.."
-echo "==> Running lints"
-rye run lint
+if [ "$1" = "--fix" ]; then
+ echo "==> Running lints with --fix"
+ rye run fix:ruff
+else
+ echo "==> Running lints"
+ rye run lint
+fi
echo "==> Making sure it imports"
rye run python -c 'import hyperspell'
diff --git a/src/hyperspell/_base_client.py b/src/hyperspell/_base_client.py
index fbf8d394..15df0a77 100644
--- a/src/hyperspell/_base_client.py
+++ b/src/hyperspell/_base_client.py
@@ -9,6 +9,7 @@
import inspect
import logging
import platform
+import warnings
import email.utils
from types import TracebackType
from random import random
@@ -51,9 +52,11 @@
ResponseT,
AnyMapping,
PostParser,
+ BinaryTypes,
RequestFiles,
HttpxSendArgs,
RequestOptions,
+ AsyncBinaryTypes,
HttpxRequestFiles,
ModelBuilderProtocol,
not_given,
@@ -477,8 +480,19 @@ def _build_request(
retries_taken: int = 0,
) -> httpx.Request:
if log.isEnabledFor(logging.DEBUG):
- log.debug("Request options: %s", model_dump(options, exclude_unset=True))
-
+ log.debug(
+ "Request options: %s",
+ model_dump(
+ options,
+ exclude_unset=True,
+ # Pydantic v1 can't dump every type we support in content, so we exclude it for now.
+ exclude={
+ "content",
+ }
+ if PYDANTIC_V1
+ else {},
+ ),
+ )
kwargs: dict[str, Any] = {}
json_data = options.json_data
@@ -532,7 +546,13 @@ def _build_request(
is_body_allowed = options.method.lower() != "get"
if is_body_allowed:
- if isinstance(json_data, bytes):
+ if options.content is not None and json_data is not None:
+ raise TypeError("Passing both `content` and `json_data` is not supported")
+ if options.content is not None and files is not None:
+ raise TypeError("Passing both `content` and `files` is not supported")
+ if options.content is not None:
+ kwargs["content"] = options.content
+ elif isinstance(json_data, bytes):
kwargs["content"] = json_data
else:
kwargs["json"] = json_data if is_given(json_data) else None
@@ -1194,6 +1214,7 @@ def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
options: RequestOptions = {},
files: RequestFiles | None = None,
stream: Literal[False] = False,
@@ -1206,6 +1227,7 @@ def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
options: RequestOptions = {},
files: RequestFiles | None = None,
stream: Literal[True],
@@ -1219,6 +1241,7 @@ def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
options: RequestOptions = {},
files: RequestFiles | None = None,
stream: bool,
@@ -1231,13 +1254,25 @@ def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
options: RequestOptions = {},
files: RequestFiles | None = None,
stream: bool = False,
stream_cls: type[_StreamT] | None = None,
) -> ResponseT | _StreamT:
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if files is not None and content is not None:
+ raise TypeError("Passing both `files` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
opts = FinalRequestOptions.construct(
- method="post", url=path, json_data=body, files=to_httpx_files(files), **options
+ method="post", url=path, json_data=body, content=content, files=to_httpx_files(files), **options
)
return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))
@@ -1247,9 +1282,24 @@ def patch(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
+ files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
- opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options)
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if files is not None and content is not None:
+ raise TypeError("Passing both `files` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ opts = FinalRequestOptions.construct(
+ method="patch", url=path, json_data=body, content=content, files=to_httpx_files(files), **options
+ )
return self.request(cast_to, opts)
def put(
@@ -1258,11 +1308,23 @@ def put(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if files is not None and content is not None:
+ raise TypeError("Passing both `files` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
opts = FinalRequestOptions.construct(
- method="put", url=path, json_data=body, files=to_httpx_files(files), **options
+ method="put", url=path, json_data=body, content=content, files=to_httpx_files(files), **options
)
return self.request(cast_to, opts)
@@ -1272,9 +1334,19 @@ def delete(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
options: RequestOptions = {},
) -> ResponseT:
- opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options)
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options)
return self.request(cast_to, opts)
def get_api_list(
@@ -1714,6 +1786,7 @@ async def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
stream: Literal[False] = False,
@@ -1726,6 +1799,7 @@ async def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
stream: Literal[True],
@@ -1739,6 +1813,7 @@ async def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
stream: bool,
@@ -1751,13 +1826,25 @@ async def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
stream: bool = False,
stream_cls: type[_AsyncStreamT] | None = None,
) -> ResponseT | _AsyncStreamT:
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if files is not None and content is not None:
+ raise TypeError("Passing both `files` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
opts = FinalRequestOptions.construct(
- method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options
+ method="post", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options
)
return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)
@@ -1767,9 +1854,29 @@ async def patch(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
+ files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
- opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options)
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if files is not None and content is not None:
+ raise TypeError("Passing both `files` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ opts = FinalRequestOptions.construct(
+ method="patch",
+ url=path,
+ json_data=body,
+ content=content,
+ files=await async_to_httpx_files(files),
+ **options,
+ )
return await self.request(cast_to, opts)
async def put(
@@ -1778,11 +1885,23 @@ async def put(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if files is not None and content is not None:
+ raise TypeError("Passing both `files` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
opts = FinalRequestOptions.construct(
- method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options
+ method="put", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options
)
return await self.request(cast_to, opts)
@@ -1792,9 +1911,19 @@ async def delete(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
options: RequestOptions = {},
) -> ResponseT:
- opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options)
+ if body is not None and content is not None:
+ raise TypeError("Passing both `body` and `content` is not supported")
+ if isinstance(body, bytes):
+ warnings.warn(
+ "Passing raw bytes as `body` is deprecated and will be removed in a future version. "
+ "Please pass raw bytes via the `content` parameter instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options)
return await self.request(cast_to, opts)
def get_api_list(
diff --git a/src/hyperspell/_client.py b/src/hyperspell/_client.py
index a41a2c80..7d685187 100644
--- a/src/hyperspell/_client.py
+++ b/src/hyperspell/_client.py
@@ -3,7 +3,7 @@
from __future__ import annotations
import os
-from typing import Any, Mapping
+from typing import TYPE_CHECKING, Any, Mapping
from typing_extensions import Self, override
import httpx
@@ -20,8 +20,8 @@
not_given,
)
from ._utils import is_given, get_async_library
+from ._compat import cached_property
from ._version import __version__
-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 (
@@ -29,7 +29,15 @@
SyncAPIClient,
AsyncAPIClient,
)
-from .resources.integrations import integrations
+
+if TYPE_CHECKING:
+ from .resources import auth, vaults, evaluate, memories, connections, integrations
+ from .resources.auth import AuthResource, AsyncAuthResource
+ from .resources.vaults import VaultsResource, AsyncVaultsResource
+ from .resources.evaluate import EvaluateResource, AsyncEvaluateResource
+ from .resources.memories import MemoriesResource, AsyncMemoriesResource
+ from .resources.connections import ConnectionsResource, AsyncConnectionsResource
+ from .resources.integrations.integrations import IntegrationsResource, AsyncIntegrationsResource
__all__ = [
"Timeout",
@@ -44,15 +52,6 @@
class Hyperspell(SyncAPIClient):
- connections: connections.ConnectionsResource
- integrations: integrations.IntegrationsResource
- memories: memories.MemoriesResource
- evaluate: evaluate.EvaluateResource
- vaults: vaults.VaultsResource
- auth: auth.AuthResource
- with_raw_response: HyperspellWithRawResponse
- with_streaming_response: HyperspellWithStreamedResponse
-
# client options
api_key: str
user_id: str | None
@@ -111,14 +110,49 @@ 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)
- self.vaults = vaults.VaultsResource(self)
- self.auth = auth.AuthResource(self)
- self.with_raw_response = HyperspellWithRawResponse(self)
- self.with_streaming_response = HyperspellWithStreamedResponse(self)
+ @cached_property
+ def connections(self) -> ConnectionsResource:
+ from .resources.connections import ConnectionsResource
+
+ return ConnectionsResource(self)
+
+ @cached_property
+ def integrations(self) -> IntegrationsResource:
+ from .resources.integrations import IntegrationsResource
+
+ return IntegrationsResource(self)
+
+ @cached_property
+ def memories(self) -> MemoriesResource:
+ from .resources.memories import MemoriesResource
+
+ return MemoriesResource(self)
+
+ @cached_property
+ def evaluate(self) -> EvaluateResource:
+ from .resources.evaluate import EvaluateResource
+
+ return EvaluateResource(self)
+
+ @cached_property
+ def vaults(self) -> VaultsResource:
+ from .resources.vaults import VaultsResource
+
+ return VaultsResource(self)
+
+ @cached_property
+ def auth(self) -> AuthResource:
+ from .resources.auth import AuthResource
+
+ return AuthResource(self)
+
+ @cached_property
+ def with_raw_response(self) -> HyperspellWithRawResponse:
+ return HyperspellWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> HyperspellWithStreamedResponse:
+ return HyperspellWithStreamedResponse(self)
@property
@override
@@ -239,15 +273,6 @@ def _make_status_error(
class AsyncHyperspell(AsyncAPIClient):
- connections: connections.AsyncConnectionsResource
- integrations: integrations.AsyncIntegrationsResource
- memories: memories.AsyncMemoriesResource
- evaluate: evaluate.AsyncEvaluateResource
- vaults: vaults.AsyncVaultsResource
- auth: auth.AsyncAuthResource
- with_raw_response: AsyncHyperspellWithRawResponse
- with_streaming_response: AsyncHyperspellWithStreamedResponse
-
# client options
api_key: str
user_id: str | None
@@ -306,14 +331,49 @@ 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)
- self.vaults = vaults.AsyncVaultsResource(self)
- self.auth = auth.AsyncAuthResource(self)
- self.with_raw_response = AsyncHyperspellWithRawResponse(self)
- self.with_streaming_response = AsyncHyperspellWithStreamedResponse(self)
+ @cached_property
+ def connections(self) -> AsyncConnectionsResource:
+ from .resources.connections import AsyncConnectionsResource
+
+ return AsyncConnectionsResource(self)
+
+ @cached_property
+ def integrations(self) -> AsyncIntegrationsResource:
+ from .resources.integrations import AsyncIntegrationsResource
+
+ return AsyncIntegrationsResource(self)
+
+ @cached_property
+ def memories(self) -> AsyncMemoriesResource:
+ from .resources.memories import AsyncMemoriesResource
+
+ return AsyncMemoriesResource(self)
+
+ @cached_property
+ def evaluate(self) -> AsyncEvaluateResource:
+ from .resources.evaluate import AsyncEvaluateResource
+
+ return AsyncEvaluateResource(self)
+
+ @cached_property
+ def vaults(self) -> AsyncVaultsResource:
+ from .resources.vaults import AsyncVaultsResource
+
+ return AsyncVaultsResource(self)
+
+ @cached_property
+ def auth(self) -> AsyncAuthResource:
+ from .resources.auth import AsyncAuthResource
+
+ return AsyncAuthResource(self)
+
+ @cached_property
+ def with_raw_response(self) -> AsyncHyperspellWithRawResponse:
+ return AsyncHyperspellWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncHyperspellWithStreamedResponse:
+ return AsyncHyperspellWithStreamedResponse(self)
@property
@override
@@ -434,43 +494,175 @@ def _make_status_error(
class HyperspellWithRawResponse:
+ _client: Hyperspell
+
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)
- self.vaults = vaults.VaultsResourceWithRawResponse(client.vaults)
- self.auth = auth.AuthResourceWithRawResponse(client.auth)
+ self._client = client
+
+ @cached_property
+ def connections(self) -> connections.ConnectionsResourceWithRawResponse:
+ from .resources.connections import ConnectionsResourceWithRawResponse
+
+ return ConnectionsResourceWithRawResponse(self._client.connections)
+
+ @cached_property
+ def integrations(self) -> integrations.IntegrationsResourceWithRawResponse:
+ from .resources.integrations import IntegrationsResourceWithRawResponse
+
+ return IntegrationsResourceWithRawResponse(self._client.integrations)
+
+ @cached_property
+ def memories(self) -> memories.MemoriesResourceWithRawResponse:
+ from .resources.memories import MemoriesResourceWithRawResponse
+
+ return MemoriesResourceWithRawResponse(self._client.memories)
+
+ @cached_property
+ def evaluate(self) -> evaluate.EvaluateResourceWithRawResponse:
+ from .resources.evaluate import EvaluateResourceWithRawResponse
+
+ return EvaluateResourceWithRawResponse(self._client.evaluate)
+
+ @cached_property
+ def vaults(self) -> vaults.VaultsResourceWithRawResponse:
+ from .resources.vaults import VaultsResourceWithRawResponse
+
+ return VaultsResourceWithRawResponse(self._client.vaults)
+
+ @cached_property
+ def auth(self) -> auth.AuthResourceWithRawResponse:
+ from .resources.auth import AuthResourceWithRawResponse
+
+ return AuthResourceWithRawResponse(self._client.auth)
class AsyncHyperspellWithRawResponse:
+ _client: AsyncHyperspell
+
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)
- self.vaults = vaults.AsyncVaultsResourceWithRawResponse(client.vaults)
- self.auth = auth.AsyncAuthResourceWithRawResponse(client.auth)
+ self._client = client
+
+ @cached_property
+ def connections(self) -> connections.AsyncConnectionsResourceWithRawResponse:
+ from .resources.connections import AsyncConnectionsResourceWithRawResponse
+
+ return AsyncConnectionsResourceWithRawResponse(self._client.connections)
+
+ @cached_property
+ def integrations(self) -> integrations.AsyncIntegrationsResourceWithRawResponse:
+ from .resources.integrations import AsyncIntegrationsResourceWithRawResponse
+
+ return AsyncIntegrationsResourceWithRawResponse(self._client.integrations)
+
+ @cached_property
+ def memories(self) -> memories.AsyncMemoriesResourceWithRawResponse:
+ from .resources.memories import AsyncMemoriesResourceWithRawResponse
+
+ return AsyncMemoriesResourceWithRawResponse(self._client.memories)
+
+ @cached_property
+ def evaluate(self) -> evaluate.AsyncEvaluateResourceWithRawResponse:
+ from .resources.evaluate import AsyncEvaluateResourceWithRawResponse
+
+ return AsyncEvaluateResourceWithRawResponse(self._client.evaluate)
+
+ @cached_property
+ def vaults(self) -> vaults.AsyncVaultsResourceWithRawResponse:
+ from .resources.vaults import AsyncVaultsResourceWithRawResponse
+
+ return AsyncVaultsResourceWithRawResponse(self._client.vaults)
+
+ @cached_property
+ def auth(self) -> auth.AsyncAuthResourceWithRawResponse:
+ from .resources.auth import AsyncAuthResourceWithRawResponse
+
+ return AsyncAuthResourceWithRawResponse(self._client.auth)
class HyperspellWithStreamedResponse:
+ _client: Hyperspell
+
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)
- self.vaults = vaults.VaultsResourceWithStreamingResponse(client.vaults)
- self.auth = auth.AuthResourceWithStreamingResponse(client.auth)
+ self._client = client
+
+ @cached_property
+ def connections(self) -> connections.ConnectionsResourceWithStreamingResponse:
+ from .resources.connections import ConnectionsResourceWithStreamingResponse
+
+ return ConnectionsResourceWithStreamingResponse(self._client.connections)
+
+ @cached_property
+ def integrations(self) -> integrations.IntegrationsResourceWithStreamingResponse:
+ from .resources.integrations import IntegrationsResourceWithStreamingResponse
+
+ return IntegrationsResourceWithStreamingResponse(self._client.integrations)
+
+ @cached_property
+ def memories(self) -> memories.MemoriesResourceWithStreamingResponse:
+ from .resources.memories import MemoriesResourceWithStreamingResponse
+
+ return MemoriesResourceWithStreamingResponse(self._client.memories)
+
+ @cached_property
+ def evaluate(self) -> evaluate.EvaluateResourceWithStreamingResponse:
+ from .resources.evaluate import EvaluateResourceWithStreamingResponse
+
+ return EvaluateResourceWithStreamingResponse(self._client.evaluate)
+
+ @cached_property
+ def vaults(self) -> vaults.VaultsResourceWithStreamingResponse:
+ from .resources.vaults import VaultsResourceWithStreamingResponse
+
+ return VaultsResourceWithStreamingResponse(self._client.vaults)
+
+ @cached_property
+ def auth(self) -> auth.AuthResourceWithStreamingResponse:
+ from .resources.auth import AuthResourceWithStreamingResponse
+
+ return AuthResourceWithStreamingResponse(self._client.auth)
class AsyncHyperspellWithStreamedResponse:
+ _client: AsyncHyperspell
+
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)
- self.vaults = vaults.AsyncVaultsResourceWithStreamingResponse(client.vaults)
- self.auth = auth.AsyncAuthResourceWithStreamingResponse(client.auth)
+ self._client = client
+
+ @cached_property
+ def connections(self) -> connections.AsyncConnectionsResourceWithStreamingResponse:
+ from .resources.connections import AsyncConnectionsResourceWithStreamingResponse
+
+ return AsyncConnectionsResourceWithStreamingResponse(self._client.connections)
+
+ @cached_property
+ def integrations(self) -> integrations.AsyncIntegrationsResourceWithStreamingResponse:
+ from .resources.integrations import AsyncIntegrationsResourceWithStreamingResponse
+
+ return AsyncIntegrationsResourceWithStreamingResponse(self._client.integrations)
+
+ @cached_property
+ def memories(self) -> memories.AsyncMemoriesResourceWithStreamingResponse:
+ from .resources.memories import AsyncMemoriesResourceWithStreamingResponse
+
+ return AsyncMemoriesResourceWithStreamingResponse(self._client.memories)
+
+ @cached_property
+ def evaluate(self) -> evaluate.AsyncEvaluateResourceWithStreamingResponse:
+ from .resources.evaluate import AsyncEvaluateResourceWithStreamingResponse
+
+ return AsyncEvaluateResourceWithStreamingResponse(self._client.evaluate)
+
+ @cached_property
+ def vaults(self) -> vaults.AsyncVaultsResourceWithStreamingResponse:
+ from .resources.vaults import AsyncVaultsResourceWithStreamingResponse
+
+ return AsyncVaultsResourceWithStreamingResponse(self._client.vaults)
+
+ @cached_property
+ def auth(self) -> auth.AsyncAuthResourceWithStreamingResponse:
+ from .resources.auth import AsyncAuthResourceWithStreamingResponse
+
+ return AsyncAuthResourceWithStreamingResponse(self._client.auth)
Client = Hyperspell
diff --git a/src/hyperspell/_models.py b/src/hyperspell/_models.py
index ca9500b2..29070e05 100644
--- a/src/hyperspell/_models.py
+++ b/src/hyperspell/_models.py
@@ -3,7 +3,20 @@
import os
import inspect
import weakref
-from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast
+from typing import (
+ IO,
+ TYPE_CHECKING,
+ Any,
+ Type,
+ Union,
+ Generic,
+ TypeVar,
+ Callable,
+ Iterable,
+ Optional,
+ AsyncIterable,
+ cast,
+)
from datetime import date, datetime
from typing_extensions import (
List,
@@ -787,6 +800,7 @@ class FinalRequestOptionsInput(TypedDict, total=False):
timeout: float | Timeout | None
files: HttpxRequestFiles | None
idempotency_key: str
+ content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None]
json_data: Body
extra_json: AnyMapping
follow_redirects: bool
@@ -805,6 +819,7 @@ class FinalRequestOptions(pydantic.BaseModel):
post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven()
follow_redirects: Union[bool, None] = None
+ content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None
# It should be noted that we cannot use `json` here as that would override
# a BaseModel method in an incompatible fashion.
json_data: Union[Body, None] = None
diff --git a/src/hyperspell/_types.py b/src/hyperspell/_types.py
index 59d3b796..c331e84c 100644
--- a/src/hyperspell/_types.py
+++ b/src/hyperspell/_types.py
@@ -13,9 +13,11 @@
Mapping,
TypeVar,
Callable,
+ Iterable,
Iterator,
Optional,
Sequence,
+ AsyncIterable,
)
from typing_extensions import (
Set,
@@ -56,6 +58,13 @@
else:
Base64FileInput = Union[IO[bytes], PathLike]
FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8.
+
+
+# Used for sending raw binary data / streaming data in request bodies
+# e.g. for file uploads without multipart encoding
+BinaryTypes = Union[bytes, bytearray, IO[bytes], Iterable[bytes]]
+AsyncBinaryTypes = Union[bytes, bytearray, IO[bytes], AsyncIterable[bytes]]
+
FileTypes = Union[
# file (or bytes)
FileContent,
diff --git a/src/hyperspell/_version.py b/src/hyperspell/_version.py
index 1223a2c6..aee8df02 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.28.0" # x-release-please-version
+__version__ = "0.30.0" # x-release-please-version
diff --git a/src/hyperspell/resources/memories.py b/src/hyperspell/resources/memories.py
index 5d558e23..b18948db 100644
--- a/src/hyperspell/resources/memories.py
+++ b/src/hyperspell/resources/memories.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from typing import Dict, List, Union, Mapping, Optional, cast
+from typing import Dict, List, Union, Mapping, Iterable, Optional, cast
from datetime import datetime
from typing_extensions import Literal
@@ -14,6 +14,7 @@
memory_search_params,
memory_update_params,
memory_upload_params,
+ memory_add_bulk_params,
)
from .._types import Body, Omit, Query, Headers, NotGiven, FileTypes, omit, not_given
from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
@@ -32,6 +33,7 @@
from ..types.shared.query_result import QueryResult
from ..types.memory_delete_response import MemoryDeleteResponse
from ..types.memory_status_response import MemoryStatusResponse
+from ..types.memory_add_bulk_response import MemoryAddBulkResponse
__all__ = ["MemoriesResource", "AsyncMemoriesResource"]
@@ -62,51 +64,15 @@ def update(
*,
source: Literal[
"collections",
- "vault",
- "web_crawler",
+ "reddit",
"notion",
"slack",
"google_calendar",
- "reddit",
+ "google_mail",
"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",
+ "vault",
+ "web_crawler",
],
collection: Union[str, object, None] | Omit = omit,
metadata: Union[Dict[str, Union[str, float, bool]], object, None] | Omit = omit,
@@ -177,51 +143,15 @@ def list(
source: Optional[
Literal[
"collections",
- "vault",
- "web_crawler",
+ "reddit",
"notion",
"slack",
"google_calendar",
- "reddit",
+ "google_mail",
"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",
+ "vault",
+ "web_crawler",
]
]
| Omit = omit,
@@ -282,51 +212,15 @@ def delete(
*,
source: Literal[
"collections",
- "vault",
- "web_crawler",
+ "reddit",
"notion",
"slack",
"google_calendar",
- "reddit",
+ "google_mail",
"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",
+ "vault",
+ "web_crawler",
],
# 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.
@@ -440,57 +334,62 @@ def add(
cast_to=MemoryStatus,
)
+ def add_bulk(
+ self,
+ *,
+ items: Iterable[memory_add_bulk_params.Item],
+ # 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,
+ ) -> MemoryAddBulkResponse:
+ """
+ Adds multiple documents to the index in a single request.
+
+ All items are validated before any database operations occur. If any item fails
+ validation, the entire batch is rejected with a 422 error detailing which items
+ failed and why.
+
+ Maximum 100 items per request. Each item follows the same schema as the
+ single-item /memories/add endpoint.
+
+ Args:
+ items: List of memories to ingest. Maximum 100 items.
+
+ 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
+ """
+ return self._post(
+ "/memories/add/bulk",
+ body=maybe_transform({"items": items}, memory_add_bulk_params.MemoryAddBulkParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=MemoryAddBulkResponse,
+ )
+
def get(
self,
resource_id: str,
*,
source: Literal[
"collections",
- "vault",
- "web_crawler",
+ "reddit",
"notion",
"slack",
"google_calendar",
- "reddit",
+ "google_mail",
"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",
+ "vault",
+ "web_crawler",
],
# 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.
@@ -533,51 +432,15 @@ def search(
sources: List[
Literal[
"collections",
- "vault",
- "web_crawler",
+ "reddit",
"notion",
"slack",
"google_calendar",
- "reddit",
+ "google_mail",
"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",
+ "vault",
+ "web_crawler",
]
]
| Omit = omit,
@@ -735,51 +598,15 @@ async def update(
*,
source: Literal[
"collections",
- "vault",
- "web_crawler",
+ "reddit",
"notion",
"slack",
"google_calendar",
- "reddit",
+ "google_mail",
"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",
+ "vault",
+ "web_crawler",
],
collection: Union[str, object, None] | Omit = omit,
metadata: Union[Dict[str, Union[str, float, bool]], object, None] | Omit = omit,
@@ -850,51 +677,15 @@ def list(
source: Optional[
Literal[
"collections",
- "vault",
- "web_crawler",
+ "reddit",
"notion",
"slack",
"google_calendar",
- "reddit",
+ "google_mail",
"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",
+ "vault",
+ "web_crawler",
]
]
| Omit = omit,
@@ -955,51 +746,15 @@ async def delete(
*,
source: Literal[
"collections",
- "vault",
- "web_crawler",
+ "reddit",
"notion",
"slack",
"google_calendar",
- "reddit",
+ "google_mail",
"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",
+ "vault",
+ "web_crawler",
],
# 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.
@@ -1113,57 +868,62 @@ async def add(
cast_to=MemoryStatus,
)
+ async def add_bulk(
+ self,
+ *,
+ items: Iterable[memory_add_bulk_params.Item],
+ # 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,
+ ) -> MemoryAddBulkResponse:
+ """
+ Adds multiple documents to the index in a single request.
+
+ All items are validated before any database operations occur. If any item fails
+ validation, the entire batch is rejected with a 422 error detailing which items
+ failed and why.
+
+ Maximum 100 items per request. Each item follows the same schema as the
+ single-item /memories/add endpoint.
+
+ Args:
+ items: List of memories to ingest. Maximum 100 items.
+
+ 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
+ """
+ return await self._post(
+ "/memories/add/bulk",
+ body=await async_maybe_transform({"items": items}, memory_add_bulk_params.MemoryAddBulkParams),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=MemoryAddBulkResponse,
+ )
+
async def get(
self,
resource_id: str,
*,
source: Literal[
"collections",
- "vault",
- "web_crawler",
+ "reddit",
"notion",
"slack",
"google_calendar",
- "reddit",
+ "google_mail",
"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",
+ "vault",
+ "web_crawler",
],
# 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.
@@ -1206,51 +966,15 @@ async def search(
sources: List[
Literal[
"collections",
- "vault",
- "web_crawler",
+ "reddit",
"notion",
"slack",
"google_calendar",
- "reddit",
+ "google_mail",
"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",
+ "vault",
+ "web_crawler",
]
]
| Omit = omit,
@@ -1398,6 +1122,9 @@ def __init__(self, memories: MemoriesResource) -> None:
self.add = to_raw_response_wrapper(
memories.add,
)
+ self.add_bulk = to_raw_response_wrapper(
+ memories.add_bulk,
+ )
self.get = to_raw_response_wrapper(
memories.get,
)
@@ -1428,6 +1155,9 @@ def __init__(self, memories: AsyncMemoriesResource) -> None:
self.add = async_to_raw_response_wrapper(
memories.add,
)
+ self.add_bulk = async_to_raw_response_wrapper(
+ memories.add_bulk,
+ )
self.get = async_to_raw_response_wrapper(
memories.get,
)
@@ -1458,6 +1188,9 @@ def __init__(self, memories: MemoriesResource) -> None:
self.add = to_streamed_response_wrapper(
memories.add,
)
+ self.add_bulk = to_streamed_response_wrapper(
+ memories.add_bulk,
+ )
self.get = to_streamed_response_wrapper(
memories.get,
)
@@ -1488,6 +1221,9 @@ def __init__(self, memories: AsyncMemoriesResource) -> None:
self.add = async_to_streamed_response_wrapper(
memories.add,
)
+ self.add_bulk = async_to_streamed_response_wrapper(
+ memories.add_bulk,
+ )
self.get = async_to_streamed_response_wrapper(
memories.get,
)
diff --git a/src/hyperspell/types/__init__.py b/src/hyperspell/types/__init__.py
index 434ea44e..64dcb9a9 100644
--- a/src/hyperspell/types/__init__.py
+++ b/src/hyperspell/types/__init__.py
@@ -15,9 +15,11 @@
from .memory_update_params import MemoryUpdateParams as MemoryUpdateParams
from .memory_upload_params import MemoryUploadParams as MemoryUploadParams
from .auth_user_token_params import AuthUserTokenParams as AuthUserTokenParams
+from .memory_add_bulk_params import MemoryAddBulkParams as MemoryAddBulkParams
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 .memory_add_bulk_response import MemoryAddBulkResponse as MemoryAddBulkResponse
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
diff --git a/src/hyperspell/types/auth_me_response.py b/src/hyperspell/types/auth_me_response.py
index 7eee2ac6..7cbf9af8 100644
--- a/src/hyperspell/types/auth_me_response.py
+++ b/src/hyperspell/types/auth_me_response.py
@@ -35,51 +35,15 @@ class AuthMeResponse(BaseModel):
available_integrations: List[
Literal[
"collections",
- "vault",
- "web_crawler",
+ "reddit",
"notion",
"slack",
"google_calendar",
- "reddit",
+ "google_mail",
"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",
+ "vault",
+ "web_crawler",
]
]
"""All integrations available for the app"""
@@ -87,51 +51,15 @@ class AuthMeResponse(BaseModel):
installed_integrations: List[
Literal[
"collections",
- "vault",
- "web_crawler",
+ "reddit",
"notion",
"slack",
"google_calendar",
- "reddit",
+ "google_mail",
"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",
+ "vault",
+ "web_crawler",
]
]
"""All integrations installed for the user"""
diff --git a/src/hyperspell/types/connection_list_response.py b/src/hyperspell/types/connection_list_response.py
index 0a60e0e6..8dec1b08 100644
--- a/src/hyperspell/types/connection_list_response.py
+++ b/src/hyperspell/types/connection_list_response.py
@@ -20,51 +20,15 @@ class Connection(BaseModel):
provider: Literal[
"collections",
- "vault",
- "web_crawler",
+ "reddit",
"notion",
"slack",
"google_calendar",
- "reddit",
+ "google_mail",
"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",
+ "vault",
+ "web_crawler",
]
"""The connection's provider"""
diff --git a/src/hyperspell/types/integration_list_response.py b/src/hyperspell/types/integration_list_response.py
index c189654f..e48fd87b 100644
--- a/src/hyperspell/types/integration_list_response.py
+++ b/src/hyperspell/types/integration_list_response.py
@@ -15,7 +15,7 @@ class Integration(BaseModel):
allow_multiple_connections: bool
"""Whether the integration allows multiple connections"""
- auth_provider: Literal["nango", "hyperspell", "composio", "whitelabel", "unified"]
+ auth_provider: Literal["nango", "unified", "whitelabel"]
"""The integration's auth provider"""
icon: str
@@ -26,51 +26,15 @@ class Integration(BaseModel):
provider: Literal[
"collections",
- "vault",
- "web_crawler",
+ "reddit",
"notion",
"slack",
"google_calendar",
- "reddit",
+ "google_mail",
"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",
+ "vault",
+ "web_crawler",
]
"""The integration's provider"""
diff --git a/src/hyperspell/types/integrations/web_crawler_index_response.py b/src/hyperspell/types/integrations/web_crawler_index_response.py
index deddf6e7..58a33f01 100644
--- a/src/hyperspell/types/integrations/web_crawler_index_response.py
+++ b/src/hyperspell/types/integrations/web_crawler_index_response.py
@@ -12,51 +12,15 @@ class WebCrawlerIndexResponse(BaseModel):
source: Literal[
"collections",
- "vault",
- "web_crawler",
+ "reddit",
"notion",
"slack",
"google_calendar",
- "reddit",
+ "google_mail",
"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",
+ "vault",
+ "web_crawler",
]
status: Literal["pending", "processing", "completed", "failed"]
diff --git a/src/hyperspell/types/memory.py b/src/hyperspell/types/memory.py
index df0de529..a03bab39 100644
--- a/src/hyperspell/types/memory.py
+++ b/src/hyperspell/types/memory.py
@@ -50,51 +50,15 @@ class Memory(BaseModel):
source: Literal[
"collections",
- "vault",
- "web_crawler",
+ "reddit",
"notion",
"slack",
"google_calendar",
- "reddit",
+ "google_mail",
"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",
+ "vault",
+ "web_crawler",
]
metadata: Optional[Metadata] = None
diff --git a/src/hyperspell/types/memory_add_bulk_params.py b/src/hyperspell/types/memory_add_bulk_params.py
new file mode 100644
index 00000000..1422a541
--- /dev/null
+++ b/src/hyperspell/types/memory_add_bulk_params.py
@@ -0,0 +1,50 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import Dict, Union, Iterable, Optional
+from datetime import datetime
+from typing_extensions import Required, Annotated, TypedDict
+
+from .._utils import PropertyInfo
+
+__all__ = ["MemoryAddBulkParams", "Item"]
+
+
+class MemoryAddBulkParams(TypedDict, total=False):
+ items: Required[Iterable[Item]]
+ """List of memories to ingest. Maximum 100 items."""
+
+
+class Item(TypedDict, total=False):
+ text: Required[str]
+ """Full text of the document."""
+
+ collection: Optional[str]
+ """The collection to add the document to for easier retrieval."""
+
+ date: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")]
+ """Date of the document.
+
+ Depending on the document, this could be the creation date or date the document
+ was last updated (eg. for a chat transcript, this would be the date of the last
+ message). This helps the ranking algorithm and allows you to filter by date
+ 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.
+
+ If not provided, a new resource ID will be generated. If provided, the document
+ will be updated if it already exists.
+ """
+
+ title: Optional[str]
+ """Title of the document."""
diff --git a/src/hyperspell/types/memory_add_bulk_response.py b/src/hyperspell/types/memory_add_bulk_response.py
new file mode 100644
index 00000000..0cb65978
--- /dev/null
+++ b/src/hyperspell/types/memory_add_bulk_response.py
@@ -0,0 +1,20 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List, Optional
+
+from .._models import BaseModel
+from .memory_status import MemoryStatus
+
+__all__ = ["MemoryAddBulkResponse"]
+
+
+class MemoryAddBulkResponse(BaseModel):
+ """Response schema for successful bulk ingestion."""
+
+ count: int
+ """Number of items successfully processed"""
+
+ items: List[MemoryStatus]
+ """Status of each ingested item"""
+
+ success: Optional[bool] = None
diff --git a/src/hyperspell/types/memory_delete_response.py b/src/hyperspell/types/memory_delete_response.py
index 7f5fbd62..19319f8b 100644
--- a/src/hyperspell/types/memory_delete_response.py
+++ b/src/hyperspell/types/memory_delete_response.py
@@ -16,51 +16,15 @@ class MemoryDeleteResponse(BaseModel):
source: Literal[
"collections",
- "vault",
- "web_crawler",
+ "reddit",
"notion",
"slack",
"google_calendar",
- "reddit",
+ "google_mail",
"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",
+ "vault",
+ "web_crawler",
]
success: bool
diff --git a/src/hyperspell/types/memory_list_params.py b/src/hyperspell/types/memory_list_params.py
index 5e01df30..55a5e339 100644
--- a/src/hyperspell/types/memory_list_params.py
+++ b/src/hyperspell/types/memory_list_params.py
@@ -25,51 +25,15 @@ class MemoryListParams(TypedDict, total=False):
source: Optional[
Literal[
"collections",
- "vault",
- "web_crawler",
+ "reddit",
"notion",
"slack",
"google_calendar",
- "reddit",
+ "google_mail",
"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",
+ "vault",
+ "web_crawler",
]
]
"""Filter documents by source."""
diff --git a/src/hyperspell/types/memory_search_params.py b/src/hyperspell/types/memory_search_params.py
index a12d2359..d3b51e49 100644
--- a/src/hyperspell/types/memory_search_params.py
+++ b/src/hyperspell/types/memory_search_params.py
@@ -40,51 +40,15 @@ class MemorySearchParams(TypedDict, total=False):
sources: List[
Literal[
"collections",
- "vault",
- "web_crawler",
+ "reddit",
"notion",
"slack",
"google_calendar",
- "reddit",
+ "google_mail",
"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",
+ "vault",
+ "web_crawler",
]
]
"""Only query documents from these sources."""
diff --git a/src/hyperspell/types/memory_status.py b/src/hyperspell/types/memory_status.py
index 3473d663..586933fb 100644
--- a/src/hyperspell/types/memory_status.py
+++ b/src/hyperspell/types/memory_status.py
@@ -12,51 +12,15 @@ class MemoryStatus(BaseModel):
source: Literal[
"collections",
- "vault",
- "web_crawler",
+ "reddit",
"notion",
"slack",
"google_calendar",
- "reddit",
+ "google_mail",
"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",
+ "vault",
+ "web_crawler",
]
status: Literal["pending", "processing", "completed", "failed"]
diff --git a/src/hyperspell/types/memory_update_params.py b/src/hyperspell/types/memory_update_params.py
index 60ef2743..ece1a7a5 100644
--- a/src/hyperspell/types/memory_update_params.py
+++ b/src/hyperspell/types/memory_update_params.py
@@ -12,51 +12,15 @@ class MemoryUpdateParams(TypedDict, total=False):
source: Required[
Literal[
"collections",
- "vault",
- "web_crawler",
+ "reddit",
"notion",
"slack",
"google_calendar",
- "reddit",
+ "google_mail",
"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",
+ "vault",
+ "web_crawler",
]
]
diff --git a/tests/api_resources/test_memories.py b/tests/api_resources/test_memories.py
index 36f9f677..4a13bb22 100644
--- a/tests/api_resources/test_memories.py
+++ b/tests/api_resources/test_memories.py
@@ -14,6 +14,7 @@
MemoryStatus,
MemoryDeleteResponse,
MemoryStatusResponse,
+ MemoryAddBulkResponse,
)
from hyperspell._utils import parse_datetime
from hyperspell.pagination import SyncCursorPage, AsyncCursorPage
@@ -200,6 +201,37 @@ def test_streaming_response_add(self, client: Hyperspell) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ def test_method_add_bulk(self, client: Hyperspell) -> None:
+ memory = client.memories.add_bulk(
+ items=[{"text": "..."}],
+ )
+ assert_matches_type(MemoryAddBulkResponse, memory, path=["response"])
+
+ @parametrize
+ def test_raw_response_add_bulk(self, client: Hyperspell) -> None:
+ response = client.memories.with_raw_response.add_bulk(
+ items=[{"text": "..."}],
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ memory = response.parse()
+ assert_matches_type(MemoryAddBulkResponse, memory, path=["response"])
+
+ @parametrize
+ def test_streaming_response_add_bulk(self, client: Hyperspell) -> None:
+ with client.memories.with_streaming_response.add_bulk(
+ items=[{"text": "..."}],
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ memory = response.parse()
+ assert_matches_type(MemoryAddBulkResponse, memory, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
@parametrize
def test_method_get(self, client: Hyperspell) -> None:
memory = client.memories.get(
@@ -603,6 +635,37 @@ async def test_streaming_response_add(self, async_client: AsyncHyperspell) -> No
assert cast(Any, response.is_closed) is True
+ @parametrize
+ async def test_method_add_bulk(self, async_client: AsyncHyperspell) -> None:
+ memory = await async_client.memories.add_bulk(
+ items=[{"text": "..."}],
+ )
+ assert_matches_type(MemoryAddBulkResponse, memory, path=["response"])
+
+ @parametrize
+ async def test_raw_response_add_bulk(self, async_client: AsyncHyperspell) -> None:
+ response = await async_client.memories.with_raw_response.add_bulk(
+ items=[{"text": "..."}],
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ memory = await response.parse()
+ assert_matches_type(MemoryAddBulkResponse, memory, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_add_bulk(self, async_client: AsyncHyperspell) -> None:
+ async with async_client.memories.with_streaming_response.add_bulk(
+ items=[{"text": "..."}],
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ memory = await response.parse()
+ assert_matches_type(MemoryAddBulkResponse, memory, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
@parametrize
async def test_method_get(self, async_client: AsyncHyperspell) -> None:
memory = await async_client.memories.get(
diff --git a/tests/test_client.py b/tests/test_client.py
index 770b4bba..f3aef279 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -8,10 +8,11 @@
import json
import asyncio
import inspect
+import dataclasses
import tracemalloc
-from typing import Any, Union, cast
+from typing import Any, Union, TypeVar, Callable, Iterable, Iterator, Optional, Coroutine, cast
from unittest import mock
-from typing_extensions import Literal
+from typing_extensions import Literal, AsyncIterator, override
import httpx
import pytest
@@ -36,6 +37,7 @@
from .utils import update_env
+T = TypeVar("T")
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
api_key = "My API Key"
user_id = "My User ID"
@@ -51,6 +53,57 @@ def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float:
return 0.1
+def mirror_request_content(request: httpx.Request) -> httpx.Response:
+ return httpx.Response(200, content=request.content)
+
+
+# note: we can't use the httpx.MockTransport class as it consumes the request
+# body itself, which means we can't test that the body is read lazily
+class MockTransport(httpx.BaseTransport, httpx.AsyncBaseTransport):
+ def __init__(
+ self,
+ handler: Callable[[httpx.Request], httpx.Response]
+ | Callable[[httpx.Request], Coroutine[Any, Any, httpx.Response]],
+ ) -> None:
+ self.handler = handler
+
+ @override
+ def handle_request(
+ self,
+ request: httpx.Request,
+ ) -> httpx.Response:
+ assert not inspect.iscoroutinefunction(self.handler), "handler must not be a coroutine function"
+ assert inspect.isfunction(self.handler), "handler must be a function"
+ return self.handler(request)
+
+ @override
+ async def handle_async_request(
+ self,
+ request: httpx.Request,
+ ) -> httpx.Response:
+ assert inspect.iscoroutinefunction(self.handler), "handler must be a coroutine function"
+ return await self.handler(request)
+
+
+@dataclasses.dataclass
+class Counter:
+ value: int = 0
+
+
+def _make_sync_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> Iterator[T]:
+ for item in iterable:
+ if counter:
+ counter.value += 1
+ yield item
+
+
+async def _make_async_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> AsyncIterator[T]:
+ for item in iterable:
+ if counter:
+ counter.value += 1
+ yield item
+
+
def _get_open_connections(client: Hyperspell | AsyncHyperspell) -> int:
transport = client._client._transport
assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport)
@@ -543,6 +596,71 @@ def test_multipart_repeating_array(self, client: Hyperspell) -> None:
b"",
]
+ @pytest.mark.respx(base_url=base_url)
+ def test_binary_content_upload(self, respx_mock: MockRouter, client: Hyperspell) -> None:
+ respx_mock.post("/upload").mock(side_effect=mirror_request_content)
+
+ file_content = b"Hello, this is a test file."
+
+ response = client.post(
+ "/upload",
+ content=file_content,
+ cast_to=httpx.Response,
+ options={"headers": {"Content-Type": "application/octet-stream"}},
+ )
+
+ assert response.status_code == 200
+ assert response.request.headers["Content-Type"] == "application/octet-stream"
+ assert response.content == file_content
+
+ def test_binary_content_upload_with_iterator(self) -> None:
+ file_content = b"Hello, this is a test file."
+ counter = Counter()
+ iterator = _make_sync_iterator([file_content], counter=counter)
+
+ def mock_handler(request: httpx.Request) -> httpx.Response:
+ assert counter.value == 0, "the request body should not have been read"
+ return httpx.Response(200, content=request.read())
+
+ with Hyperspell(
+ base_url=base_url,
+ api_key=api_key,
+ user_id=user_id,
+ _strict_response_validation=True,
+ http_client=httpx.Client(transport=MockTransport(handler=mock_handler)),
+ ) as client:
+ response = client.post(
+ "/upload",
+ content=iterator,
+ cast_to=httpx.Response,
+ options={"headers": {"Content-Type": "application/octet-stream"}},
+ )
+
+ assert response.status_code == 200
+ assert response.request.headers["Content-Type"] == "application/octet-stream"
+ assert response.content == file_content
+ assert counter.value == 1
+
+ @pytest.mark.respx(base_url=base_url)
+ def test_binary_content_upload_with_body_is_deprecated(self, respx_mock: MockRouter, client: Hyperspell) -> None:
+ respx_mock.post("/upload").mock(side_effect=mirror_request_content)
+
+ file_content = b"Hello, this is a test file."
+
+ with pytest.deprecated_call(
+ match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead."
+ ):
+ response = client.post(
+ "/upload",
+ body=file_content,
+ cast_to=httpx.Response,
+ options={"headers": {"Content-Type": "application/octet-stream"}},
+ )
+
+ assert response.status_code == 200
+ assert response.request.headers["Content-Type"] == "application/octet-stream"
+ assert response.content == file_content
+
@pytest.mark.respx(base_url=base_url)
def test_basic_union_response(self, respx_mock: MockRouter, client: Hyperspell) -> None:
class Model1(BaseModel):
@@ -1434,6 +1552,73 @@ def test_multipart_repeating_array(self, async_client: AsyncHyperspell) -> None:
b"",
]
+ @pytest.mark.respx(base_url=base_url)
+ async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncHyperspell) -> None:
+ respx_mock.post("/upload").mock(side_effect=mirror_request_content)
+
+ file_content = b"Hello, this is a test file."
+
+ response = await async_client.post(
+ "/upload",
+ content=file_content,
+ cast_to=httpx.Response,
+ options={"headers": {"Content-Type": "application/octet-stream"}},
+ )
+
+ assert response.status_code == 200
+ assert response.request.headers["Content-Type"] == "application/octet-stream"
+ assert response.content == file_content
+
+ async def test_binary_content_upload_with_asynciterator(self) -> None:
+ file_content = b"Hello, this is a test file."
+ counter = Counter()
+ iterator = _make_async_iterator([file_content], counter=counter)
+
+ async def mock_handler(request: httpx.Request) -> httpx.Response:
+ assert counter.value == 0, "the request body should not have been read"
+ return httpx.Response(200, content=await request.aread())
+
+ async with AsyncHyperspell(
+ base_url=base_url,
+ api_key=api_key,
+ user_id=user_id,
+ _strict_response_validation=True,
+ http_client=httpx.AsyncClient(transport=MockTransport(handler=mock_handler)),
+ ) as client:
+ response = await client.post(
+ "/upload",
+ content=iterator,
+ cast_to=httpx.Response,
+ options={"headers": {"Content-Type": "application/octet-stream"}},
+ )
+
+ assert response.status_code == 200
+ assert response.request.headers["Content-Type"] == "application/octet-stream"
+ assert response.content == file_content
+ assert counter.value == 1
+
+ @pytest.mark.respx(base_url=base_url)
+ async def test_binary_content_upload_with_body_is_deprecated(
+ self, respx_mock: MockRouter, async_client: AsyncHyperspell
+ ) -> None:
+ respx_mock.post("/upload").mock(side_effect=mirror_request_content)
+
+ file_content = b"Hello, this is a test file."
+
+ with pytest.deprecated_call(
+ match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead."
+ ):
+ response = await async_client.post(
+ "/upload",
+ body=file_content,
+ cast_to=httpx.Response,
+ options={"headers": {"Content-Type": "application/octet-stream"}},
+ )
+
+ assert response.status_code == 200
+ assert response.request.headers["Content-Type"] == "application/octet-stream"
+ assert response.content == file_content
+
@pytest.mark.respx(base_url=base_url)
async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncHyperspell) -> None:
class Model1(BaseModel):