diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7e334a01..e6a005eb 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: |
@@ -63,7 +63,7 @@ jobs:
- name: Get GitHub OIDC Token
if: github.repository == 'stainless-sdks/hyperspell-python'
id: github-oidc
- uses: actions/github-script@v6
+ uses: actions/github-script@v8
with:
script: core.setOutput('github_token', await core.getIDToken());
@@ -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..737339ac 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-34d6c3c2efa9b9b88d95ff49ff64f45440720b14a046edf3fac953d6eb0d13a1.yml
+openapi_spec_hash: 80ce00b9f30af244e0a331c3430ed5e4
+config_hash: bd77d0b7029518c697756456d6854f07
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e0dab305..625df7a2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,37 @@
# Changelog
+## 0.30.0 (2026-01-24)
+
+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 ([a1721fa](https://github.com/hyperspell/python-sdk/commit/a1721fac778acdbb3d05c71396da3277e201cde0))
+* **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
+
+* **ci:** upgrade `actions/github-script` ([87a2eff](https://github.com/hyperspell/python-sdk/commit/87a2effc14173b8ee2d74f2f4424a79423449f39))
+* **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..7ebf9449 100644
--- a/api.md
+++ b/api.md
@@ -65,16 +65,24 @@ Methods:
Types:
```python
-from hyperspell.types import Memory, MemoryStatus, MemoryDeleteResponse, MemoryStatusResponse
+from hyperspell.types import (
+ MemoryStatus,
+ MemoryListResponse,
+ MemoryDeleteResponse,
+ MemoryAddBulkResponse,
+ MemoryGetResponse,
+ MemoryStatusResponse,
+)
```
Methods:
- client.memories.update(resource_id, \*, source, \*\*params) -> MemoryStatus
-- client.memories.list(\*\*params) -> SyncCursorPage[Memory]
+- client.memories.list(\*\*params) -> SyncCursorPage[MemoryListResponse]
- client.memories.delete(resource_id, \*, source) -> MemoryDeleteResponse
- client.memories.add(\*\*params) -> MemoryStatus
-- client.memories.get(resource_id, \*, source) -> Memory
+- client.memories.add_bulk(\*\*params) -> MemoryAddBulkResponse
+- client.memories.get(resource_id, \*, source) -> MemoryGetResponse
- client.memories.search(\*\*params) -> QueryResult
- client.memories.status() -> MemoryStatusResponse
- client.memories.upload(\*\*params) -> MemoryStatus
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..e407dd98 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
@@ -27,11 +28,13 @@
)
from ..pagination import SyncCursorPage, AsyncCursorPage
from .._base_client import AsyncPaginator, make_request_options
-from ..types.memory import Memory
from ..types.memory_status import MemoryStatus
+from ..types.memory_get_response import MemoryGetResponse
from ..types.shared.query_result import QueryResult
+from ..types.memory_list_response import MemoryListResponse
from ..types.memory_delete_response import MemoryDeleteResponse
from ..types.memory_status_response import MemoryStatusResponse
+from ..types.memory_add_bulk_response import MemoryAddBulkResponse
__all__ = ["MemoriesResource", "AsyncMemoriesResource"]
@@ -62,51 +65,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 +144,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,
@@ -231,7 +162,7 @@ def list(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> SyncCursorPage[Memory]:
+ ) -> SyncCursorPage[MemoryListResponse]:
"""This endpoint allows you to paginate through all documents in the index.
You can
@@ -256,7 +187,7 @@ def list(
"""
return self._get_api_list(
"/memories/list",
- page=SyncCursorPage[Memory],
+ page=SyncCursorPage[MemoryListResponse],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -273,7 +204,7 @@ def list(
memory_list_params.MemoryListParams,
),
),
- model=Memory,
+ model=MemoryListResponse,
)
def delete(
@@ -282,51 +213,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 +335,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.
@@ -498,7 +398,7 @@ def get(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> Memory:
+ ) -> MemoryGetResponse:
"""
Retrieves a document by provider and resource_id.
@@ -520,7 +420,7 @@ def get(
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=Memory,
+ cast_to=MemoryGetResponse,
)
def search(
@@ -533,51 +433,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 +599,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 +678,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,
@@ -904,7 +696,7 @@ def list(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> AsyncPaginator[Memory, AsyncCursorPage[Memory]]:
+ ) -> AsyncPaginator[MemoryListResponse, AsyncCursorPage[MemoryListResponse]]:
"""This endpoint allows you to paginate through all documents in the index.
You can
@@ -929,7 +721,7 @@ def list(
"""
return self._get_api_list(
"/memories/list",
- page=AsyncCursorPage[Memory],
+ page=AsyncCursorPage[MemoryListResponse],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -946,7 +738,7 @@ def list(
memory_list_params.MemoryListParams,
),
),
- model=Memory,
+ model=MemoryListResponse,
)
async def delete(
@@ -955,51 +747,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 +869,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.
@@ -1171,7 +932,7 @@ async def get(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> Memory:
+ ) -> MemoryGetResponse:
"""
Retrieves a document by provider and resource_id.
@@ -1193,7 +954,7 @@ async def get(
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
- cast_to=Memory,
+ cast_to=MemoryGetResponse,
)
async def search(
@@ -1206,51 +967,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 +1123,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 +1156,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 +1189,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 +1222,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..a6d7523e 100644
--- a/src/hyperspell/types/__init__.py
+++ b/src/hyperspell/types/__init__.py
@@ -3,21 +3,24 @@
from __future__ import annotations
from .token import Token as Token
-from .memory import Memory as Memory
from .shared import QueryResult as QueryResult
from .memory_status import MemoryStatus as MemoryStatus
from .auth_me_response import AuthMeResponse as AuthMeResponse
from .memory_add_params import MemoryAddParams as MemoryAddParams
from .vault_list_params import VaultListParams as VaultListParams
from .memory_list_params import MemoryListParams as MemoryListParams
+from .memory_get_response import MemoryGetResponse as MemoryGetResponse
from .vault_list_response import VaultListResponse as VaultListResponse
+from .memory_list_response import MemoryListResponse as MemoryListResponse
from .memory_search_params import MemorySearchParams as MemorySearchParams
from .memory_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_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.py b/src/hyperspell/types/memory_get_response.py
similarity index 69%
rename from src/hyperspell/types/memory.py
rename to src/hyperspell/types/memory_get_response.py
index df0de529..384271c4 100644
--- a/src/hyperspell/types/memory.py
+++ b/src/hyperspell/types/memory_get_response.py
@@ -8,7 +8,7 @@
from .._models import BaseModel
-__all__ = ["Memory", "Metadata", "MetadataEvent"]
+__all__ = ["MemoryGetResponse", "Metadata", "MetadataEvent"]
class MetadataEvent(BaseModel):
@@ -45,56 +45,20 @@ def __getattr__(self, attr: str) -> object: ...
__pydantic_extra__: Dict[str, object]
-class Memory(BaseModel):
+class MemoryGetResponse(BaseModel):
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",
]
metadata: Optional[Metadata] = None
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_list_response.py b/src/hyperspell/types/memory_list_response.py
new file mode 100644
index 00000000..c618306d
--- /dev/null
+++ b/src/hyperspell/types/memory_list_response.py
@@ -0,0 +1,69 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import TYPE_CHECKING, Dict, List, Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
+
+from .._models import BaseModel
+
+__all__ = ["MemoryListResponse", "Metadata", "MetadataEvent"]
+
+
+class MetadataEvent(BaseModel):
+ message: str
+
+ type: Literal["error", "warning", "info", "success"]
+
+ time: Optional[datetime] = None
+
+
+class Metadata(BaseModel):
+ created_at: Optional[datetime] = None
+
+ events: Optional[List[MetadataEvent]] = None
+
+ indexed_at: Optional[datetime] = None
+
+ last_modified: Optional[datetime] = None
+
+ status: Optional[Literal["pending", "processing", "completed", "failed"]] = None
+
+ url: Optional[str] = None
+
+ if TYPE_CHECKING:
+ # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a
+ # value to this field, so for compatibility we avoid doing it at runtime.
+ __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride]
+
+ # Stub to indicate that arbitrary properties are accepted.
+ # To access properties that are not valid identifiers you can use `getattr`, e.g.
+ # `getattr(obj, '$type')`
+ def __getattr__(self, attr: str) -> object: ...
+ else:
+ __pydantic_extra__: Dict[str, object]
+
+
+class MemoryListResponse(BaseModel):
+ resource_id: str
+
+ source: Literal[
+ "collections",
+ "reddit",
+ "notion",
+ "slack",
+ "google_calendar",
+ "google_mail",
+ "box",
+ "google_drive",
+ "vault",
+ "web_crawler",
+ ]
+
+ metadata: Optional[Metadata] = None
+
+ score: Optional[float] = None
+ """The relevance of the resource to the query"""
+
+ title: Optional[str] = None
diff --git a/src/hyperspell/types/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/src/hyperspell/types/shared/query_result.py b/src/hyperspell/types/shared/query_result.py
index 67fdb09c..7a0b83fd 100644
--- a/src/hyperspell/types/shared/query_result.py
+++ b/src/hyperspell/types/shared/query_result.py
@@ -1,15 +1,76 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import Dict, List, Optional
+from typing import TYPE_CHECKING, Dict, List, Optional
+from datetime import datetime
+from typing_extensions import Literal
+
+from pydantic import Field as FieldInfo
-from ..memory import Memory
from ..._models import BaseModel
-__all__ = ["QueryResult"]
+__all__ = ["QueryResult", "Document", "DocumentMetadata", "DocumentMetadataEvent"]
+
+
+class DocumentMetadataEvent(BaseModel):
+ message: str
+
+ type: Literal["error", "warning", "info", "success"]
+
+ time: Optional[datetime] = None
+
+
+class DocumentMetadata(BaseModel):
+ created_at: Optional[datetime] = None
+
+ events: Optional[List[DocumentMetadataEvent]] = None
+
+ indexed_at: Optional[datetime] = None
+
+ last_modified: Optional[datetime] = None
+
+ status: Optional[Literal["pending", "processing", "completed", "failed"]] = None
+
+ url: Optional[str] = None
+
+ if TYPE_CHECKING:
+ # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a
+ # value to this field, so for compatibility we avoid doing it at runtime.
+ __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride]
+
+ # Stub to indicate that arbitrary properties are accepted.
+ # To access properties that are not valid identifiers you can use `getattr`, e.g.
+ # `getattr(obj, '$type')`
+ def __getattr__(self, attr: str) -> object: ...
+ else:
+ __pydantic_extra__: Dict[str, object]
+
+
+class Document(BaseModel):
+ resource_id: str
+
+ source: Literal[
+ "collections",
+ "reddit",
+ "notion",
+ "slack",
+ "google_calendar",
+ "google_mail",
+ "box",
+ "google_drive",
+ "vault",
+ "web_crawler",
+ ]
+
+ metadata: Optional[DocumentMetadata] = None
+
+ score: Optional[float] = None
+ """The relevance of the resource to the query"""
+
+ title: Optional[str] = None
class QueryResult(BaseModel):
- documents: List[Memory]
+ documents: List[Document]
answer: Optional[str] = None
"""The answer to the query, if the request was set to answer."""
diff --git a/tests/api_resources/test_memories.py b/tests/api_resources/test_memories.py
index 36f9f677..190854c5 100644
--- a/tests/api_resources/test_memories.py
+++ b/tests/api_resources/test_memories.py
@@ -10,10 +10,12 @@
from hyperspell import Hyperspell, AsyncHyperspell
from tests.utils import assert_matches_type
from hyperspell.types import (
- Memory,
MemoryStatus,
+ MemoryGetResponse,
+ MemoryListResponse,
MemoryDeleteResponse,
MemoryStatusResponse,
+ MemoryAddBulkResponse,
)
from hyperspell._utils import parse_datetime
from hyperspell.pagination import SyncCursorPage, AsyncCursorPage
@@ -82,7 +84,7 @@ def test_path_params_update(self, client: Hyperspell) -> None:
@parametrize
def test_method_list(self, client: Hyperspell) -> None:
memory = client.memories.list()
- assert_matches_type(SyncCursorPage[Memory], memory, path=["response"])
+ assert_matches_type(SyncCursorPage[MemoryListResponse], memory, path=["response"])
@parametrize
def test_method_list_with_all_params(self, client: Hyperspell) -> None:
@@ -93,7 +95,7 @@ def test_method_list_with_all_params(self, client: Hyperspell) -> None:
size=0,
source="collections",
)
- assert_matches_type(SyncCursorPage[Memory], memory, path=["response"])
+ assert_matches_type(SyncCursorPage[MemoryListResponse], memory, path=["response"])
@parametrize
def test_raw_response_list(self, client: Hyperspell) -> None:
@@ -102,7 +104,7 @@ def test_raw_response_list(self, client: Hyperspell) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = response.parse()
- assert_matches_type(SyncCursorPage[Memory], memory, path=["response"])
+ assert_matches_type(SyncCursorPage[MemoryListResponse], memory, path=["response"])
@parametrize
def test_streaming_response_list(self, client: Hyperspell) -> None:
@@ -111,7 +113,7 @@ def test_streaming_response_list(self, client: Hyperspell) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = response.parse()
- assert_matches_type(SyncCursorPage[Memory], memory, path=["response"])
+ assert_matches_type(SyncCursorPage[MemoryListResponse], memory, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -200,13 +202,44 @@ 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(
resource_id="resource_id",
source="collections",
)
- assert_matches_type(Memory, memory, path=["response"])
+ assert_matches_type(MemoryGetResponse, memory, path=["response"])
@parametrize
def test_raw_response_get(self, client: Hyperspell) -> None:
@@ -218,7 +251,7 @@ def test_raw_response_get(self, client: Hyperspell) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = response.parse()
- assert_matches_type(Memory, memory, path=["response"])
+ assert_matches_type(MemoryGetResponse, memory, path=["response"])
@parametrize
def test_streaming_response_get(self, client: Hyperspell) -> None:
@@ -230,7 +263,7 @@ def test_streaming_response_get(self, client: Hyperspell) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = response.parse()
- assert_matches_type(Memory, memory, path=["response"])
+ assert_matches_type(MemoryGetResponse, memory, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -485,7 +518,7 @@ async def test_path_params_update(self, async_client: AsyncHyperspell) -> None:
@parametrize
async def test_method_list(self, async_client: AsyncHyperspell) -> None:
memory = await async_client.memories.list()
- assert_matches_type(AsyncCursorPage[Memory], memory, path=["response"])
+ assert_matches_type(AsyncCursorPage[MemoryListResponse], memory, path=["response"])
@parametrize
async def test_method_list_with_all_params(self, async_client: AsyncHyperspell) -> None:
@@ -496,7 +529,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncHyperspell)
size=0,
source="collections",
)
- assert_matches_type(AsyncCursorPage[Memory], memory, path=["response"])
+ assert_matches_type(AsyncCursorPage[MemoryListResponse], memory, path=["response"])
@parametrize
async def test_raw_response_list(self, async_client: AsyncHyperspell) -> None:
@@ -505,7 +538,7 @@ async def test_raw_response_list(self, async_client: AsyncHyperspell) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = await response.parse()
- assert_matches_type(AsyncCursorPage[Memory], memory, path=["response"])
+ assert_matches_type(AsyncCursorPage[MemoryListResponse], memory, path=["response"])
@parametrize
async def test_streaming_response_list(self, async_client: AsyncHyperspell) -> None:
@@ -514,7 +547,7 @@ async def test_streaming_response_list(self, async_client: AsyncHyperspell) -> N
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = await response.parse()
- assert_matches_type(AsyncCursorPage[Memory], memory, path=["response"])
+ assert_matches_type(AsyncCursorPage[MemoryListResponse], memory, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -603,13 +636,44 @@ 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(
resource_id="resource_id",
source="collections",
)
- assert_matches_type(Memory, memory, path=["response"])
+ assert_matches_type(MemoryGetResponse, memory, path=["response"])
@parametrize
async def test_raw_response_get(self, async_client: AsyncHyperspell) -> None:
@@ -621,7 +685,7 @@ async def test_raw_response_get(self, async_client: AsyncHyperspell) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = await response.parse()
- assert_matches_type(Memory, memory, path=["response"])
+ assert_matches_type(MemoryGetResponse, memory, path=["response"])
@parametrize
async def test_streaming_response_get(self, async_client: AsyncHyperspell) -> None:
@@ -633,7 +697,7 @@ async def test_streaming_response_get(self, async_client: AsyncHyperspell) -> No
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
memory = await response.parse()
- assert_matches_type(Memory, memory, path=["response"])
+ assert_matches_type(MemoryGetResponse, memory, path=["response"])
assert cast(Any, response.is_closed) is True
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):