From 7a8e495ee6b8f162dce5c3124b007d2d60f6cdd6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 00:30:14 +0000 Subject: [PATCH 01/34] feat(api): api update --- .stats.yml | 4 ++-- src/hyperspell/resources/memories.py | 8 ++++++++ src/hyperspell/types/memory_update_params.py | 8 +++++++- tests/api_resources/test_memories.py | 2 ++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 60ac02f6..73fab0a4 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-6d6dbb68dd9021348431b28e08378d086b3eaf5e65b3dfa03125b1fdec417fa6.yml -openapi_spec_hash: 6ad2b84ac07c482fe838929694e49015 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-864ec17bc07aef2b5985bc5e876c8a99a26c8f53cd0cee693deafbe752b6b7e1.yml +openapi_spec_hash: f1d9dbac709b8de23a5243f7ccaa984f config_hash: bd8505e17db740d82e578d0edaa9bfe0 diff --git a/src/hyperspell/resources/memories.py b/src/hyperspell/resources/memories.py index d731f93a..6f750b38 100644 --- a/src/hyperspell/resources/memories.py +++ b/src/hyperspell/resources/memories.py @@ -80,6 +80,7 @@ def update( "gmail_actions", ], collection: Union[str, object, None] | Omit = omit, + date: Union[Union[str, datetime], object, None] | Omit = omit, metadata: Union[Dict[str, Union[str, float, bool, None]], object, None] | Omit = omit, text: Union[str, object, None] | Omit = omit, title: Union[str, object, None] | Omit = omit, @@ -102,6 +103,8 @@ def update( collection: The collection to move the document to — deprecated, set the collection using metadata instead. + date: Date of the document for ranking and filtering. + metadata: Custom metadata for filtering. Keys must be alphanumeric with underscores, max 64 chars. Values must be string, number, boolean, or null. Will be merged with existing metadata. @@ -127,6 +130,7 @@ def update( body=maybe_transform( { "collection": collection, + "date": date, "metadata": metadata, "text": text, "title": title, @@ -647,6 +651,7 @@ async def update( "gmail_actions", ], collection: Union[str, object, None] | Omit = omit, + date: Union[Union[str, datetime], object, None] | Omit = omit, metadata: Union[Dict[str, Union[str, float, bool, None]], object, None] | Omit = omit, text: Union[str, object, None] | Omit = omit, title: Union[str, object, None] | Omit = omit, @@ -669,6 +674,8 @@ async def update( collection: The collection to move the document to — deprecated, set the collection using metadata instead. + date: Date of the document for ranking and filtering. + metadata: Custom metadata for filtering. Keys must be alphanumeric with underscores, max 64 chars. Values must be string, number, boolean, or null. Will be merged with existing metadata. @@ -694,6 +701,7 @@ async def update( body=await async_maybe_transform( { "collection": collection, + "date": date, "metadata": metadata, "text": text, "title": title, diff --git a/src/hyperspell/types/memory_update_params.py b/src/hyperspell/types/memory_update_params.py index d8859d62..c7a19384 100644 --- a/src/hyperspell/types/memory_update_params.py +++ b/src/hyperspell/types/memory_update_params.py @@ -3,7 +3,10 @@ from __future__ import annotations from typing import Dict, Union -from typing_extensions import Literal, Required, TypedDict +from datetime import datetime +from typing_extensions import Literal, Required, Annotated, TypedDict + +from .._utils import PropertyInfo __all__ = ["MemoryUpdateParams"] @@ -34,6 +37,9 @@ class MemoryUpdateParams(TypedDict, total=False): metadata instead. """ + date: Annotated[Union[Union[str, datetime], object, None], PropertyInfo(format="iso8601")] + """Date of the document for ranking and filtering.""" + metadata: Union[Dict[str, Union[str, float, bool, None]], object, None] """Custom metadata for filtering. diff --git a/tests/api_resources/test_memories.py b/tests/api_resources/test_memories.py index 2a983b3f..b16232f1 100644 --- a/tests/api_resources/test_memories.py +++ b/tests/api_resources/test_memories.py @@ -40,6 +40,7 @@ def test_method_update_with_all_params(self, client: Hyperspell) -> None: resource_id="resource_id", source="reddit", collection="string", + date=parse_datetime("2019-12-27T18:11:19.117Z"), metadata={"foo": "string"}, text="string", title="string", @@ -449,6 +450,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncHyperspell resource_id="resource_id", source="reddit", collection="string", + date=parse_datetime("2019-12-27T18:11:19.117Z"), metadata={"foo": "string"}, text="string", title="string", From c28640c64d25fd5dde07836e60e970069b0b65fe Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 08:14:18 +0000 Subject: [PATCH 02/34] perf(client): optimize file structure copying in multipart requests --- src/hyperspell/_files.py | 56 +++++++++++++++- src/hyperspell/_utils/__init__.py | 1 - src/hyperspell/_utils/_utils.py | 15 ----- src/hyperspell/resources/memories.py | 13 ++-- tests/test_deepcopy.py | 58 ---------------- tests/test_files.py | 99 +++++++++++++++++++++++++++- 6 files changed, 159 insertions(+), 83 deletions(-) delete mode 100644 tests/test_deepcopy.py diff --git a/src/hyperspell/_files.py b/src/hyperspell/_files.py index 155adfec..34fa5459 100644 --- a/src/hyperspell/_files.py +++ b/src/hyperspell/_files.py @@ -3,8 +3,8 @@ import io import os import pathlib -from typing import overload -from typing_extensions import TypeGuard +from typing import Sequence, cast, overload +from typing_extensions import TypeVar, TypeGuard import anyio @@ -17,7 +17,9 @@ HttpxFileContent, HttpxRequestFiles, ) -from ._utils import is_tuple_t, is_mapping_t, is_sequence_t +from ._utils import is_list, is_mapping, is_tuple_t, is_mapping_t, is_sequence_t + +_T = TypeVar("_T") def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]: @@ -121,3 +123,51 @@ async def async_read_file_content(file: FileContent) -> HttpxFileContent: return await anyio.Path(file).read_bytes() return file + + +def deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]]) -> _T: + """Copy only the containers along the given paths. + + Used to guard against mutation by extract_files without copying the entire structure. + Only dicts and lists that lie on a path are copied; everything else + is returned by reference. + + For example, given paths=[["foo", "files", "file"]] and the structure: + { + "foo": { + "bar": {"baz": {}}, + "files": {"file": } + } + } + The root dict, "foo", and "files" are copied (they lie on the path). + "bar" and "baz" are returned by reference (off the path). + """ + return _deepcopy_with_paths(item, paths, 0) + + +def _deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]], index: int) -> _T: + if not paths: + return item + if is_mapping(item): + key_to_paths: dict[str, list[Sequence[str]]] = {} + for path in paths: + if index < len(path): + key_to_paths.setdefault(path[index], []).append(path) + + # if no path continues through this mapping, it won't be mutated and copying it is redundant + if not key_to_paths: + return item + + result = dict(item) + for key, subpaths in key_to_paths.items(): + if key in result: + result[key] = _deepcopy_with_paths(result[key], subpaths, index + 1) + return cast(_T, result) + if is_list(item): + array_paths = [path for path in paths if index < len(path) and path[index] == ""] + + # if no path expects a list here, nothing will be mutated inside it - return by reference + if not array_paths: + return cast(_T, item) + return cast(_T, [_deepcopy_with_paths(entry, array_paths, index + 1) for entry in item]) + return item diff --git a/src/hyperspell/_utils/__init__.py b/src/hyperspell/_utils/__init__.py index 10cb66d2..1c090e51 100644 --- a/src/hyperspell/_utils/__init__.py +++ b/src/hyperspell/_utils/__init__.py @@ -24,7 +24,6 @@ coerce_integer as coerce_integer, file_from_path as file_from_path, strip_not_given as strip_not_given, - deepcopy_minimal as deepcopy_minimal, get_async_library as get_async_library, maybe_coerce_float as maybe_coerce_float, get_required_header as get_required_header, diff --git a/src/hyperspell/_utils/_utils.py b/src/hyperspell/_utils/_utils.py index 63b8cd60..771859f5 100644 --- a/src/hyperspell/_utils/_utils.py +++ b/src/hyperspell/_utils/_utils.py @@ -177,21 +177,6 @@ def is_iterable(obj: object) -> TypeGuard[Iterable[object]]: return isinstance(obj, Iterable) -def deepcopy_minimal(item: _T) -> _T: - """Minimal reimplementation of copy.deepcopy() that will only copy certain object types: - - - mappings, e.g. `dict` - - list - - This is done for performance reasons. - """ - if is_mapping(item): - return cast(_T, {k: deepcopy_minimal(v) for k, v in item.items()}) - if is_list(item): - return cast(_T, [deepcopy_minimal(entry) for entry in item]) - return item - - # copied from https://github.com/Rapptz/RoboDanny def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str: size = len(seq) diff --git a/src/hyperspell/resources/memories.py b/src/hyperspell/resources/memories.py index 6f750b38..03a2875d 100644 --- a/src/hyperspell/resources/memories.py +++ b/src/hyperspell/resources/memories.py @@ -16,8 +16,9 @@ memory_upload_params, memory_add_bulk_params, ) +from .._files import deepcopy_with_paths from .._types import Body, Omit, Query, Headers, NotGiven, FileTypes, omit, not_given -from .._utils import extract_files, path_template, maybe_transform, deepcopy_minimal, async_maybe_transform +from .._utils import extract_files, path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -587,12 +588,13 @@ def upload( timeout: Override the client-level default timeout for this request, in seconds """ - body = deepcopy_minimal( + body = deepcopy_with_paths( { "file": file, "collection": collection, "metadata": metadata, - } + }, + [["file"]], ) files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be @@ -1158,12 +1160,13 @@ async def upload( timeout: Override the client-level default timeout for this request, in seconds """ - body = deepcopy_minimal( + body = deepcopy_with_paths( { "file": file, "collection": collection, "metadata": metadata, - } + }, + [["file"]], ) files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py deleted file mode 100644 index 93ca9c04..00000000 --- a/tests/test_deepcopy.py +++ /dev/null @@ -1,58 +0,0 @@ -from hyperspell._utils import deepcopy_minimal - - -def assert_different_identities(obj1: object, obj2: object) -> None: - assert obj1 == obj2 - assert id(obj1) != id(obj2) - - -def test_simple_dict() -> None: - obj1 = {"foo": "bar"} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - - -def test_nested_dict() -> None: - obj1 = {"foo": {"bar": True}} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1["foo"], obj2["foo"]) - - -def test_complex_nested_dict() -> None: - obj1 = {"foo": {"bar": [{"hello": "world"}]}} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1["foo"], obj2["foo"]) - assert_different_identities(obj1["foo"]["bar"], obj2["foo"]["bar"]) - assert_different_identities(obj1["foo"]["bar"][0], obj2["foo"]["bar"][0]) - - -def test_simple_list() -> None: - obj1 = ["a", "b", "c"] - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - - -def test_nested_list() -> None: - obj1 = ["a", [1, 2, 3]] - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1[1], obj2[1]) - - -class MyObject: ... - - -def test_ignores_other_types() -> None: - # custom classes - my_obj = MyObject() - obj1 = {"foo": my_obj} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert obj1["foo"] is my_obj - - # tuples - obj3 = ("a", "b") - obj4 = deepcopy_minimal(obj3) - assert obj3 is obj4 diff --git a/tests/test_files.py b/tests/test_files.py index aae00cb3..e51c3d06 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -4,7 +4,8 @@ import pytest from dirty_equals import IsDict, IsList, IsBytes, IsTuple -from hyperspell._files import to_httpx_files, async_to_httpx_files +from hyperspell._files import to_httpx_files, deepcopy_with_paths, async_to_httpx_files +from hyperspell._utils import extract_files readme_path = Path(__file__).parent.parent.joinpath("README.md") @@ -49,3 +50,99 @@ def test_string_not_allowed() -> None: "file": "foo", # type: ignore } ) + + +def assert_different_identities(obj1: object, obj2: object) -> None: + assert obj1 == obj2 + assert obj1 is not obj2 + + +class TestDeepcopyWithPaths: + def test_copies_top_level_dict(self) -> None: + original = {"file": b"data", "other": "value"} + result = deepcopy_with_paths(original, [["file"]]) + assert_different_identities(result, original) + + def test_file_value_is_same_reference(self) -> None: + file_bytes = b"contents" + original = {"file": file_bytes} + result = deepcopy_with_paths(original, [["file"]]) + assert_different_identities(result, original) + assert result["file"] is file_bytes + + def test_list_popped_wholesale(self) -> None: + files = [b"f1", b"f2"] + original = {"files": files, "title": "t"} + result = deepcopy_with_paths(original, [["files", ""]]) + assert_different_identities(result, original) + result_files = result["files"] + assert isinstance(result_files, list) + assert_different_identities(result_files, files) + + def test_nested_array_path_copies_list_and_elements(self) -> None: + elem1 = {"file": b"f1", "extra": 1} + elem2 = {"file": b"f2", "extra": 2} + original = {"items": [elem1, elem2]} + result = deepcopy_with_paths(original, [["items", "", "file"]]) + assert_different_identities(result, original) + result_items = result["items"] + assert isinstance(result_items, list) + assert_different_identities(result_items, original["items"]) + assert_different_identities(result_items[0], elem1) + assert_different_identities(result_items[1], elem2) + + def test_empty_paths_returns_same_object(self) -> None: + original = {"foo": "bar"} + result = deepcopy_with_paths(original, []) + assert result is original + + def test_multiple_paths(self) -> None: + f1 = b"file1" + f2 = b"file2" + original = {"a": f1, "b": f2, "c": "unchanged"} + result = deepcopy_with_paths(original, [["a"], ["b"]]) + assert_different_identities(result, original) + assert result["a"] is f1 + assert result["b"] is f2 + assert result["c"] is original["c"] + + def test_extract_files_does_not_mutate_original_top_level(self) -> None: + file_bytes = b"contents" + original = {"file": file_bytes, "other": "value"} + + copied = deepcopy_with_paths(original, [["file"]]) + extracted = extract_files(copied, paths=[["file"]]) + + assert extracted == [("file", file_bytes)] + assert original == {"file": file_bytes, "other": "value"} + assert copied == {"other": "value"} + + def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None: + file1 = b"f1" + file2 = b"f2" + original = { + "items": [ + {"file": file1, "extra": 1}, + {"file": file2, "extra": 2}, + ], + "title": "example", + } + + copied = deepcopy_with_paths(original, [["items", "", "file"]]) + extracted = extract_files(copied, paths=[["items", "", "file"]]) + + assert extracted == [("items[][file]", file1), ("items[][file]", file2)] + assert original == { + "items": [ + {"file": file1, "extra": 1}, + {"file": file2, "extra": 2}, + ], + "title": "example", + } + assert copied == { + "items": [ + {"extra": 1}, + {"extra": 2}, + ], + "title": "example", + } From 7d67d91802b13e3110af4e1ab313cc36ed66ca74 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 08:15:49 +0000 Subject: [PATCH 03/34] chore(tests): bump steady to v0.22.1 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 5cd7c157..feebe5ed 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.20.2 -- steady --version + npm exec --package=@stdy/cli@0.22.1 -- steady --version - npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index b754adab..a47c5b42 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.2 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.22.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 From f50d5a32152fd0a33ad5634bc72e03a6332bc293 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 18:30:28 +0000 Subject: [PATCH 04/34] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 73fab0a4..19377f47 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-864ec17bc07aef2b5985bc5e876c8a99a26c8f53cd0cee693deafbe752b6b7e1.yml -openapi_spec_hash: f1d9dbac709b8de23a5243f7ccaa984f +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-a4bf4f3aaf1509541db646bc9ff7ec58e78cb4d42cf6bf83881b02739ad77b87.yml +openapi_spec_hash: 89cd02b2290061877e6badcddb7c8eb8 config_hash: bd8505e17db740d82e578d0edaa9bfe0 From 384db7ac2a8a40d8d4e72aaebf7a5c67d2a15ac2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:20:35 +0000 Subject: [PATCH 05/34] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 19377f47..b925885d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-a4bf4f3aaf1509541db646bc9ff7ec58e78cb4d42cf6bf83881b02739ad77b87.yml openapi_spec_hash: 89cd02b2290061877e6badcddb7c8eb8 -config_hash: bd8505e17db740d82e578d0edaa9bfe0 +config_hash: 2ac853a71ed8b421da86d06bd90cf23f From 3c63a3bb5093eb0cc3fe44ac30979ab006c95bb0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:26:40 +0000 Subject: [PATCH 06/34] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index b925885d..390cd289 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-a4bf4f3aaf1509541db646bc9ff7ec58e78cb4d42cf6bf83881b02739ad77b87.yml openapi_spec_hash: 89cd02b2290061877e6badcddb7c8eb8 -config_hash: 2ac853a71ed8b421da86d06bd90cf23f +config_hash: f779e7db9263cd21efe5e9469bc1d012 From 23c9221674f7d4d4d563b3e8d80d55c0b884114a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:38:43 +0000 Subject: [PATCH 07/34] feat(api): manual updates --- .stats.yml | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 390cd289..08bee8b2 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-a4bf4f3aaf1509541db646bc9ff7ec58e78cb4d42cf6bf83881b02739ad77b87.yml openapi_spec_hash: 89cd02b2290061877e6badcddb7c8eb8 -config_hash: f779e7db9263cd21efe5e9469bc1d012 +config_hash: 597eba5e5eaec83a5f0db3d946af8db5 diff --git a/README.md b/README.md index 4b0ec922..da027aea 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ It is generated with [Stainless](https://www.stainless.com/). 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. -[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=hyperspell-mcp&config=eyJuYW1lIjoiaHlwZXJzcGVsbC1tY3AiLCJ0cmFuc3BvcnQiOiJodHRwIiwidXJsIjoiaHR0cHM6Ly9oeXBlcnNwZWxsLnN0bG1jcC5jb20iLCJoZWFkZXJzIjp7IngtaHlwZXJzcGVsbC1hcGkta2V5IjoiTXkgQVBJIEtleSIsIlgtQXMtVXNlciI6Ik15IFVzZXIgSUQifX0) -[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22hyperspell-mcp%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fhyperspell.stlmcp.com%22%2C%22headers%22%3A%7B%22x-hyperspell-api-key%22%3A%22My%20API%20Key%22%2C%22X-As-User%22%3A%22My%20User%20ID%22%7D%7D) +[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40hyperspell%2Fhyperspell-mcp&config=eyJuYW1lIjoiQGh5cGVyc3BlbGwvaHlwZXJzcGVsbC1tY3AiLCJ0cmFuc3BvcnQiOiJodHRwIiwidXJsIjoiaHR0cHM6Ly9oeXBlcnNwZWxsLnN0bG1jcC5jb20iLCJoZWFkZXJzIjp7IngtaHlwZXJzcGVsbC1hcGkta2V5IjoiTXkgQVBJIEtleSIsIlgtQXMtVXNlciI6Ik15IFVzZXIgSUQifX0) +[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40hyperspell%2Fhyperspell-mcp%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fhyperspell.stlmcp.com%22%2C%22headers%22%3A%7B%22x-hyperspell-api-key%22%3A%22My%20API%20Key%22%2C%22X-As-User%22%3A%22My%20User%20ID%22%7D%7D) > Note: You may need to set environment variables in your MCP client. From e5611df610fefaf3ed0cf98f7f53cb8aded5c8a6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 19:51:59 +0000 Subject: [PATCH 08/34] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 08bee8b2..dba5310a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-a4bf4f3aaf1509541db646bc9ff7ec58e78cb4d42cf6bf83881b02739ad77b87.yml openapi_spec_hash: 89cd02b2290061877e6badcddb7c8eb8 -config_hash: 597eba5e5eaec83a5f0db3d946af8db5 +config_hash: 09bb5ca4418f316f95d2b75ef7399cf0 From 9be7d849f4c646fd81ef78f7efe908ebd734de8d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 23:12:19 +0000 Subject: [PATCH 09/34] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index dba5310a..08bee8b2 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-a4bf4f3aaf1509541db646bc9ff7ec58e78cb4d42cf6bf83881b02739ad77b87.yml openapi_spec_hash: 89cd02b2290061877e6badcddb7c8eb8 -config_hash: 09bb5ca4418f316f95d2b75ef7399cf0 +config_hash: 597eba5e5eaec83a5f0db3d946af8db5 From d4a39dc3c6f710bf9b9c481197043eeb4a9f8ac9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 04:05:58 +0000 Subject: [PATCH 10/34] chore(internal): more robust bootstrap script --- scripts/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bootstrap b/scripts/bootstrap index 4638ec69..5a23841b 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -4,7 +4,7 @@ set -e cd "$(dirname "$0")/.." -if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "${SKIP_BREW:-}" != "1" ] && [ -t 0 ]; then brew bundle check >/dev/null 2>&1 || { echo -n "==> Install Homebrew dependencies? (y/N): " read -r response From 0fbce5916bb7f0aab207d8a4a36a43f77316c4e6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:30:22 +0000 Subject: [PATCH 11/34] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 08bee8b2..37d5f103 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-a4bf4f3aaf1509541db646bc9ff7ec58e78cb4d42cf6bf83881b02739ad77b87.yml -openapi_spec_hash: 89cd02b2290061877e6badcddb7c8eb8 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-a3b85873a872b138491163f359f262878d19b98e5ded51fb2ba5c2cc6dae8f5c.yml +openapi_spec_hash: 2914ac795845af1b80926da07bc5727f config_hash: 597eba5e5eaec83a5f0db3d946af8db5 From e7807c91fe07a6a738c82c764d66ea3cbcce03cd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 24 Apr 2026 18:30:25 +0000 Subject: [PATCH 12/34] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 37d5f103..5adbadba 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-a3b85873a872b138491163f359f262878d19b98e5ded51fb2ba5c2cc6dae8f5c.yml -openapi_spec_hash: 2914ac795845af1b80926da07bc5727f +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-69d534d74b0030f4226e05ad99a1c636cf271b664d60c4dac2daedbe59e7be0d.yml +openapi_spec_hash: e3e75812a20b93e40f599c9b865927c4 config_hash: 597eba5e5eaec83a5f0db3d946af8db5 From fcc771b7b0e0dc27bf466e3e643b97e385e7554e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 25 Apr 2026 03:30:37 +0000 Subject: [PATCH 13/34] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 5adbadba..51c308d8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-69d534d74b0030f4226e05ad99a1c636cf271b664d60c4dac2daedbe59e7be0d.yml -openapi_spec_hash: e3e75812a20b93e40f599c9b865927c4 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-0b9bf804b7e67e6743eae14b04310e7efddb54d67f7d9c02e11acccd83169255.yml +openapi_spec_hash: 0feb465dcd08ad061bf0a0ce10c9df9f config_hash: 597eba5e5eaec83a5f0db3d946af8db5 From a56dcf8b31a493d99c3fea56d7f19d7893e2dbc0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 25 Apr 2026 18:30:37 +0000 Subject: [PATCH 14/34] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 51c308d8..61d253d9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-0b9bf804b7e67e6743eae14b04310e7efddb54d67f7d9c02e11acccd83169255.yml -openapi_spec_hash: 0feb465dcd08ad061bf0a0ce10c9df9f +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-8689d273264324bb97b5d765b0b6811c655e3d25ac4f9a65f36bf8966dcf48e0.yml +openapi_spec_hash: edb49a26f338bafcc4b339c5f6618450 config_hash: 597eba5e5eaec83a5f0db3d946af8db5 From 41e71f23ad24f0f368a27ac73b9f9a873ce33c57 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 25 Apr 2026 20:30:38 +0000 Subject: [PATCH 15/34] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 61d253d9..38acd593 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-8689d273264324bb97b5d765b0b6811c655e3d25ac4f9a65f36bf8966dcf48e0.yml -openapi_spec_hash: edb49a26f338bafcc4b339c5f6618450 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-b79d6d7c41fc55e5392e94c9c3a5251b638615f5324fb9b939e9dfd829dd1da1.yml +openapi_spec_hash: 6666715c859f216551868dcf8e602dee config_hash: 597eba5e5eaec83a5f0db3d946af8db5 From 92fd43a735c10a678fb2e9b3f052c43607b6fb8a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 02:30:54 +0000 Subject: [PATCH 16/34] feat(api): api update --- .stats.yml | 4 ++-- src/hyperspell/types/memory_search_params.py | 9 +++++++++ tests/api_resources/test_memories.py | 2 ++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 38acd593..2d998a96 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-b79d6d7c41fc55e5392e94c9c3a5251b638615f5324fb9b939e9dfd829dd1da1.yml -openapi_spec_hash: 6666715c859f216551868dcf8e602dee +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-7fa6cf3d29bd35dc57fbcd164d5d059790535463f31a354a32f2628e480443d7.yml +openapi_spec_hash: a541dd122cc17cd49c5ddf64699d24c5 config_hash: 597eba5e5eaec83a5f0db3d946af8db5 diff --git a/src/hyperspell/types/memory_search_params.py b/src/hyperspell/types/memory_search_params.py index 5719262c..81819341 100644 --- a/src/hyperspell/types/memory_search_params.py +++ b/src/hyperspell/types/memory_search_params.py @@ -275,6 +275,15 @@ class Options(TypedDict, total=False): notion: OptionsNotion """Search options for Notion""" + recency_half_life_days: Optional[float] + """ + When set, multiplies each result's score by an exponential-decay factor based on + the document's most recent activity timestamp (source-reported last_modified, + falling back to document_date). A document one half-life old gets its score + halved. Resources with no recency timestamp are passed through unchanged. Leave + unset to disable. + """ + reddit: OptionsReddit """Search options for Reddit""" diff --git a/tests/api_resources/test_memories.py b/tests/api_resources/test_memories.py index b16232f1..4d801436 100644 --- a/tests/api_resources/test_memories.py +++ b/tests/api_resources/test_memories.py @@ -315,6 +315,7 @@ def test_method_search_with_all_params(self, client: Hyperspell) -> None: "notion_page_ids": ["string"], "weight": 0, }, + "recency_half_life_days": 1, "reddit": { "period": "hour", "sort": "relevance", @@ -725,6 +726,7 @@ async def test_method_search_with_all_params(self, async_client: AsyncHyperspell "notion_page_ids": ["string"], "weight": 0, }, + "recency_half_life_days": 1, "reddit": { "period": "hour", "sort": "relevance", From dd446e0c34543e07160f08e2d11bb317bed4f889 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 00:30:37 +0000 Subject: [PATCH 17/34] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 2d998a96..3433ed72 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-7fa6cf3d29bd35dc57fbcd164d5d059790535463f31a354a32f2628e480443d7.yml -openapi_spec_hash: a541dd122cc17cd49c5ddf64699d24c5 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-a9b9447c6a65c54385fe3ae40350172c7539ece91f5cf982cd474a4afd86e050.yml +openapi_spec_hash: 1a2ca5653244b4e205d3df51991b4238 config_hash: 597eba5e5eaec83a5f0db3d946af8db5 From 0f569cccc70ea96a2da668846b62bf65af449561 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 04:17:12 +0000 Subject: [PATCH 18/34] fix: use correct field name format for multipart file arrays --- src/hyperspell/_qs.py | 8 ++----- src/hyperspell/_types.py | 3 +++ src/hyperspell/_utils/_utils.py | 42 ++++++++++++++++++++++++++------- tests/test_extract_files.py | 28 ++++++++++++++++++---- tests/test_files.py | 2 +- 5 files changed, 63 insertions(+), 20 deletions(-) diff --git a/src/hyperspell/_qs.py b/src/hyperspell/_qs.py index de8c99bc..4127c19c 100644 --- a/src/hyperspell/_qs.py +++ b/src/hyperspell/_qs.py @@ -2,17 +2,13 @@ from typing import Any, List, Tuple, Union, Mapping, TypeVar from urllib.parse import parse_qs, urlencode -from typing_extensions import Literal, get_args +from typing_extensions import get_args -from ._types import NotGiven, not_given +from ._types import NotGiven, ArrayFormat, NestedFormat, not_given from ._utils import flatten _T = TypeVar("_T") - -ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] -NestedFormat = Literal["dots", "brackets"] - PrimitiveData = Union[str, int, float, bool, None] # this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"] # https://github.com/microsoft/pyright/issues/3555 diff --git a/src/hyperspell/_types.py b/src/hyperspell/_types.py index c331e84c..a0a7f696 100644 --- a/src/hyperspell/_types.py +++ b/src/hyperspell/_types.py @@ -47,6 +47,9 @@ ModelT = TypeVar("ModelT", bound=pydantic.BaseModel) _T = TypeVar("_T") +ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] +NestedFormat = Literal["dots", "brackets"] + # Approximates httpx internal ProxiesTypes and RequestFiles types # while adding support for `PathLike` instances diff --git a/src/hyperspell/_utils/_utils.py b/src/hyperspell/_utils/_utils.py index 771859f5..199cd231 100644 --- a/src/hyperspell/_utils/_utils.py +++ b/src/hyperspell/_utils/_utils.py @@ -17,11 +17,11 @@ ) from pathlib import Path from datetime import date, datetime -from typing_extensions import TypeGuard +from typing_extensions import TypeGuard, get_args import sniffio -from .._types import Omit, NotGiven, FileTypes, HeadersLike +from .._types import Omit, NotGiven, FileTypes, ArrayFormat, HeadersLike _T = TypeVar("_T") _TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) @@ -40,25 +40,45 @@ def extract_files( query: Mapping[str, object], *, paths: Sequence[Sequence[str]], + array_format: ArrayFormat = "brackets", ) -> list[tuple[str, FileTypes]]: """Recursively extract files from the given dictionary based on specified paths. A path may look like this ['foo', 'files', '', 'data']. + ``array_format`` controls how ```` segments contribute to the emitted + field name. Supported values: ``"brackets"`` (``foo[]``), ``"repeat"`` and + ``"comma"`` (``foo``), ``"indices"`` (``foo[0]``, ``foo[1]``). + Note: this mutates the given dictionary. """ files: list[tuple[str, FileTypes]] = [] for path in paths: - files.extend(_extract_items(query, path, index=0, flattened_key=None)) + files.extend(_extract_items(query, path, index=0, flattened_key=None, array_format=array_format)) return files +def _array_suffix(array_format: ArrayFormat, array_index: int) -> str: + if array_format == "brackets": + return "[]" + if array_format == "indices": + return f"[{array_index}]" + if array_format == "repeat" or array_format == "comma": + # Both repeat the bare field name for each file part; there is no + # meaningful way to comma-join binary parts. + return "" + raise NotImplementedError( + f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}" + ) + + def _extract_items( obj: object, path: Sequence[str], *, index: int, flattened_key: str | None, + array_format: ArrayFormat, ) -> list[tuple[str, FileTypes]]: try: key = path[index] @@ -75,9 +95,11 @@ def _extract_items( if is_list(obj): files: list[tuple[str, FileTypes]] = [] - for entry in obj: - assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "") - files.append((flattened_key + "[]", cast(FileTypes, entry))) + for array_index, entry in enumerate(obj): + suffix = _array_suffix(array_format, array_index) + emitted_key = (flattened_key + suffix) if flattened_key else suffix + assert_is_file_content(entry, key=emitted_key) + files.append((emitted_key, cast(FileTypes, entry))) return files assert_is_file_content(obj, key=flattened_key) @@ -106,6 +128,7 @@ def _extract_items( path, index=index, flattened_key=flattened_key, + array_format=array_format, ) elif is_list(obj): if key != "": @@ -117,9 +140,12 @@ def _extract_items( item, path, index=index, - flattened_key=flattened_key + "[]" if flattened_key is not None else "[]", + flattened_key=( + (flattened_key if flattened_key is not None else "") + _array_suffix(array_format, array_index) + ), + array_format=array_format, ) - for item in obj + for array_index, item in enumerate(obj) ] ) diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py index da9fb3d5..e17802c5 100644 --- a/tests/test_extract_files.py +++ b/tests/test_extract_files.py @@ -4,7 +4,7 @@ import pytest -from hyperspell._types import FileTypes +from hyperspell._types import FileTypes, ArrayFormat from hyperspell._utils import extract_files @@ -37,10 +37,7 @@ def test_multiple_files() -> None: def test_top_level_file_array() -> None: query = {"files": [b"file one", b"file two"], "title": "hello"} - assert extract_files(query, paths=[["files", ""]]) == [ - ("files[]", b"file one"), - ("files[]", b"file two"), - ] + assert extract_files(query, paths=[["files", ""]]) == [("files[]", b"file one"), ("files[]", b"file two")] assert query == {"title": "hello"} @@ -71,3 +68,24 @@ def test_ignores_incorrect_paths( expected: list[tuple[str, FileTypes]], ) -> None: assert extract_files(query, paths=paths) == expected + + +@pytest.mark.parametrize( + "array_format,expected_top_level,expected_nested", + [ + ("brackets", [("files[]", b"a"), ("files[]", b"b")], [("items[][file]", b"a"), ("items[][file]", b"b")]), + ("repeat", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]), + ("comma", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]), + ("indices", [("files[0]", b"a"), ("files[1]", b"b")], [("items[0][file]", b"a"), ("items[1][file]", b"b")]), + ], +) +def test_array_format_controls_file_field_names( + array_format: ArrayFormat, + expected_top_level: list[tuple[str, FileTypes]], + expected_nested: list[tuple[str, FileTypes]], +) -> None: + top_level = {"files": [b"a", b"b"]} + assert extract_files(top_level, paths=[["files", ""]], array_format=array_format) == expected_top_level + + nested = {"items": [{"file": b"a"}, {"file": b"b"}]} + assert extract_files(nested, paths=[["items", "", "file"]], array_format=array_format) == expected_nested diff --git a/tests/test_files.py b/tests/test_files.py index e51c3d06..aded237c 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -131,7 +131,7 @@ def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None: copied = deepcopy_with_paths(original, [["items", "", "file"]]) extracted = extract_files(copied, paths=[["items", "", "file"]]) - assert extracted == [("items[][file]", file1), ("items[][file]", file2)] + assert [entry for _, entry in extracted] == [file1, file2] assert original == { "items": [ {"file": file1, "extra": 1}, From eba039883cd57a0b9df0c4779d1370af59cd36ce Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 04:19:02 +0000 Subject: [PATCH 19/34] feat: support setting headers via env --- src/hyperspell/_client.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/hyperspell/_client.py b/src/hyperspell/_client.py index 78cfb089..20dbdf4a 100644 --- a/src/hyperspell/_client.py +++ b/src/hyperspell/_client.py @@ -19,7 +19,11 @@ RequestOptions, not_given, ) -from ._utils import is_given, get_async_library +from ._utils import ( + is_given, + is_mapping_t, + get_async_library, +) from ._compat import cached_property from ._version import __version__ from ._streaming import Stream as Stream, AsyncStream as AsyncStream @@ -102,6 +106,15 @@ def __init__( if base_url is None: base_url = f"https://api.hyperspell.com" + custom_headers_env = os.environ.get("HYPERSPELL_CUSTOM_HEADERS") + if custom_headers_env is not None: + parsed: dict[str, str] = {} + for line in custom_headers_env.split("\n"): + colon = line.find(":") + if colon >= 0: + parsed[line[:colon].strip()] = line[colon + 1 :].strip() + default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})} + super().__init__( version=__version__, base_url=base_url, @@ -341,6 +354,15 @@ def __init__( if base_url is None: base_url = f"https://api.hyperspell.com" + custom_headers_env = os.environ.get("HYPERSPELL_CUSTOM_HEADERS") + if custom_headers_env is not None: + parsed: dict[str, str] = {} + for line in custom_headers_env.split("\n"): + colon = line.find(":") + if colon >= 0: + parsed[line[:colon].strip()] = line[colon + 1 :].strip() + default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})} + super().__init__( version=__version__, base_url=base_url, From 36f7fd53dfda5aaead473fe141a4417df3ded9cc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:30:46 +0000 Subject: [PATCH 20/34] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 3433ed72..1c1639a7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-a9b9447c6a65c54385fe3ae40350172c7539ece91f5cf982cd474a4afd86e050.yml -openapi_spec_hash: 1a2ca5653244b4e205d3df51991b4238 +openapi_spec_hash: 0d1d6e45ba54d24c8262744d34192950 config_hash: 597eba5e5eaec83a5f0db3d946af8db5 From 34120cbbf4b232a30c138a46859d58b52eda1565 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 05:46:34 +0000 Subject: [PATCH 21/34] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 1c1639a7..ff65df7b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-a9b9447c6a65c54385fe3ae40350172c7539ece91f5cf982cd474a4afd86e050.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-e124cba28023639d4e72808379668f874ed53285fdd75ff2a7f580c12e3815f0.yml openapi_spec_hash: 0d1d6e45ba54d24c8262744d34192950 config_hash: 597eba5e5eaec83a5f0db3d946af8db5 From 84f970242e9f6cd50f57531ad820d61b2908c48b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 08:01:49 +0000 Subject: [PATCH 22/34] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index ff65df7b..9e30a0e6 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-e124cba28023639d4e72808379668f874ed53285fdd75ff2a7f580c12e3815f0.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell/hyperspell-e124cba28023639d4e72808379668f874ed53285fdd75ff2a7f580c12e3815f0.yml openapi_spec_hash: 0d1d6e45ba54d24c8262744d34192950 config_hash: 597eba5e5eaec83a5f0db3d946af8db5 From 75d80bee62bfcf63efc6410365372248ad0f17ae Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 00:30:44 +0000 Subject: [PATCH 23/34] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 9e30a0e6..1c75d4f8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell/hyperspell-e124cba28023639d4e72808379668f874ed53285fdd75ff2a7f580c12e3815f0.yml -openapi_spec_hash: 0d1d6e45ba54d24c8262744d34192950 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell/hyperspell-baf348c0f71316403deeb2d500f83b41b2a0affc38b130e0b3646f67a1166b7b.yml +openapi_spec_hash: 459a33fc87569764c4d041e37435b77a config_hash: 597eba5e5eaec83a5f0db3d946af8db5 From 3ffd91416383b948c38cce6aaa88dccff6914f37 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 05:00:01 +0000 Subject: [PATCH 24/34] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 1c75d4f8..82f42ade 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell/hyperspell-baf348c0f71316403deeb2d500f83b41b2a0affc38b130e0b3646f67a1166b7b.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell/hyperspell-65aaec91ce1035c25007430cbfd84b22a5f270da6488fcc3592fc86e320bf17a.yml openapi_spec_hash: 459a33fc87569764c4d041e37435b77a config_hash: 597eba5e5eaec83a5f0db3d946af8db5 From 19a83c32f936735d997727bda0bb0ba523f8086c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 05:04:10 +0000 Subject: [PATCH 25/34] chore(internal): reformat pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7a6fa077..9dec6210 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -154,7 +154,7 @@ show_error_codes = true # # We also exclude our `tests` as mypy doesn't always infer # types correctly and Pyright will still catch any type errors. -exclude = ['src/hyperspell/_files.py', '_dev/.*.py', 'tests/.*'] +exclude = ["src/hyperspell/_files.py", "_dev/.*.py", "tests/.*"] strict_equality = true implicit_reexport = true From 3b25c10a9f2be985dcfbfaf059f5595ea316d5f5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 19:30:57 +0000 Subject: [PATCH 26/34] feat(api): api update --- .stats.yml | 4 +-- src/hyperspell/resources/actions.py | 12 ++++++++ src/hyperspell/resources/memories.py | 30 +++++++++++++++++++ .../types/action_add_reaction_params.py | 3 ++ .../types/action_send_message_params.py | 3 ++ src/hyperspell/types/auth_me_response.py | 6 ++++ .../types/connection_list_response.py | 3 ++ .../types/integration_list_response.py | 3 ++ .../web_crawler_index_response.py | 3 ++ src/hyperspell/types/memory.py | 3 ++ .../types/memory_delete_response.py | 3 ++ src/hyperspell/types/memory_list_params.py | 3 ++ src/hyperspell/types/memory_search_params.py | 3 ++ src/hyperspell/types/memory_status.py | 3 ++ src/hyperspell/types/memory_update_params.py | 3 ++ src/hyperspell/types/shared/resource.py | 3 ++ 16 files changed, 86 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 82f42ade..4e70c53a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell/hyperspell-65aaec91ce1035c25007430cbfd84b22a5f270da6488fcc3592fc86e320bf17a.yml -openapi_spec_hash: 459a33fc87569764c4d041e37435b77a +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell/hyperspell-3ea7819d73f46347d7870c6238e12458921045b6386d6091bffe67906d7a017f.yml +openapi_spec_hash: 004b15cbe7b318ef25b91f6d45b4cba3 config_hash: 597eba5e5eaec83a5f0db3d946af8db5 diff --git a/src/hyperspell/resources/actions.py b/src/hyperspell/resources/actions.py index 2537a806..eeae142e 100644 --- a/src/hyperspell/resources/actions.py +++ b/src/hyperspell/resources/actions.py @@ -65,6 +65,9 @@ def add_reaction( "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ], timestamp: str, connection: Optional[str] | Omit = omit, @@ -133,6 +136,9 @@ def send_message( "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ], text: str, channel: Optional[str] | Omit = omit, @@ -226,6 +232,9 @@ async def add_reaction( "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ], timestamp: str, connection: Optional[str] | Omit = omit, @@ -294,6 +303,9 @@ async def send_message( "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ], text: str, channel: Optional[str] | Omit = omit, diff --git a/src/hyperspell/resources/memories.py b/src/hyperspell/resources/memories.py index 03a2875d..24fa2438 100644 --- a/src/hyperspell/resources/memories.py +++ b/src/hyperspell/resources/memories.py @@ -79,6 +79,9 @@ def update( "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ], collection: Union[str, object, None] | Omit = omit, date: Union[Union[str, datetime], object, None] | Omit = omit, @@ -167,6 +170,9 @@ def list( "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ] ] | Omit = omit, @@ -245,6 +251,9 @@ def delete( "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ], # 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. @@ -419,6 +428,9 @@ def get( "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ], # 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. @@ -475,6 +487,9 @@ def search( "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ] ] | Omit = omit, @@ -651,6 +666,9 @@ async def update( "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ], collection: Union[str, object, None] | Omit = omit, date: Union[Union[str, datetime], object, None] | Omit = omit, @@ -739,6 +757,9 @@ def list( "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ] ] | Omit = omit, @@ -817,6 +838,9 @@ async def delete( "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ], # 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. @@ -991,6 +1015,9 @@ async def get( "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ], # 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. @@ -1047,6 +1074,9 @@ async def search( "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ] ] | Omit = omit, diff --git a/src/hyperspell/types/action_add_reaction_params.py b/src/hyperspell/types/action_add_reaction_params.py index a72da448..2b0b9d67 100644 --- a/src/hyperspell/types/action_add_reaction_params.py +++ b/src/hyperspell/types/action_add_reaction_params.py @@ -31,6 +31,9 @@ class ActionAddReactionParams(TypedDict, total=False): "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ] ] """Integration provider (e.g., slack)""" diff --git a/src/hyperspell/types/action_send_message_params.py b/src/hyperspell/types/action_send_message_params.py index 2df01983..bd0fe62d 100644 --- a/src/hyperspell/types/action_send_message_params.py +++ b/src/hyperspell/types/action_send_message_params.py @@ -25,6 +25,9 @@ class ActionSendMessageParams(TypedDict, total=False): "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ] ] """Integration provider (e.g., slack)""" diff --git a/src/hyperspell/types/auth_me_response.py b/src/hyperspell/types/auth_me_response.py index 93ea0975..6188fd55 100644 --- a/src/hyperspell/types/auth_me_response.py +++ b/src/hyperspell/types/auth_me_response.py @@ -48,6 +48,9 @@ class AuthMeResponse(BaseModel): "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ] ] """All integrations available for the app""" @@ -68,6 +71,9 @@ class AuthMeResponse(BaseModel): "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ] ] """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 8092529b..e6523e55 100644 --- a/src/hyperspell/types/connection_list_response.py +++ b/src/hyperspell/types/connection_list_response.py @@ -33,6 +33,9 @@ class Connection(BaseModel): "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ] """The connection's provider""" diff --git a/src/hyperspell/types/integration_list_response.py b/src/hyperspell/types/integration_list_response.py index 47292e38..a1e9fd67 100644 --- a/src/hyperspell/types/integration_list_response.py +++ b/src/hyperspell/types/integration_list_response.py @@ -39,6 +39,9 @@ class Integration(BaseModel): "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ] """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 16dc13e3..7d911280 100644 --- a/src/hyperspell/types/integrations/web_crawler_index_response.py +++ b/src/hyperspell/types/integrations/web_crawler_index_response.py @@ -25,6 +25,9 @@ class WebCrawlerIndexResponse(BaseModel): "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ] status: Literal["pending", "processing", "completed", "failed", "pending_review", "skipped"] diff --git a/src/hyperspell/types/memory.py b/src/hyperspell/types/memory.py index 4ac829ef..d506bf55 100644 --- a/src/hyperspell/types/memory.py +++ b/src/hyperspell/types/memory.py @@ -31,6 +31,9 @@ class Memory(BaseModel): "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ] type: str diff --git a/src/hyperspell/types/memory_delete_response.py b/src/hyperspell/types/memory_delete_response.py index 5f0432d1..ad8225a8 100644 --- a/src/hyperspell/types/memory_delete_response.py +++ b/src/hyperspell/types/memory_delete_response.py @@ -29,6 +29,9 @@ class MemoryDeleteResponse(BaseModel): "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ] success: bool diff --git a/src/hyperspell/types/memory_list_params.py b/src/hyperspell/types/memory_list_params.py index 319f2917..b22c5e32 100644 --- a/src/hyperspell/types/memory_list_params.py +++ b/src/hyperspell/types/memory_list_params.py @@ -38,6 +38,9 @@ class MemoryListParams(TypedDict, total=False): "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ] ] """Filter documents by source.""" diff --git a/src/hyperspell/types/memory_search_params.py b/src/hyperspell/types/memory_search_params.py index 81819341..4b98dc25 100644 --- a/src/hyperspell/types/memory_search_params.py +++ b/src/hyperspell/types/memory_search_params.py @@ -60,6 +60,9 @@ class MemorySearchParams(TypedDict, total=False): "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ] ] """Only query documents from these sources.""" diff --git a/src/hyperspell/types/memory_status.py b/src/hyperspell/types/memory_status.py index f30b7c73..65b0bf64 100644 --- a/src/hyperspell/types/memory_status.py +++ b/src/hyperspell/types/memory_status.py @@ -25,6 +25,9 @@ class MemoryStatus(BaseModel): "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ] status: Literal["pending", "processing", "completed", "failed", "pending_review", "skipped"] diff --git a/src/hyperspell/types/memory_update_params.py b/src/hyperspell/types/memory_update_params.py index c7a19384..f935cd7d 100644 --- a/src/hyperspell/types/memory_update_params.py +++ b/src/hyperspell/types/memory_update_params.py @@ -28,6 +28,9 @@ class MemoryUpdateParams(TypedDict, total=False): "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ] ] diff --git a/src/hyperspell/types/shared/resource.py b/src/hyperspell/types/shared/resource.py index ccc16b91..4dfde146 100644 --- a/src/hyperspell/types/shared/resource.py +++ b/src/hyperspell/types/shared/resource.py @@ -27,6 +27,9 @@ class Resource(BaseModel): "trace", "microsoft_teams", "gmail_actions", + "granola", + "fathom", + "linear", ] folder_id: Optional[str] = None From b32aef88e020579eca63eba296c1cb83cadebf8b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 19:30:51 +0000 Subject: [PATCH 27/34] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4e70c53a..be9bc18a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell/hyperspell-3ea7819d73f46347d7870c6238e12458921045b6386d6091bffe67906d7a017f.yml -openapi_spec_hash: 004b15cbe7b318ef25b91f6d45b4cba3 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell/hyperspell-65556393ee1b73d7aa80b6cdf194830148bd8ca311269345c64e6a14522f68d9.yml +openapi_spec_hash: d71925df7395382b9184ee8d55b99fdf config_hash: 597eba5e5eaec83a5f0db3d946af8db5 From bbfc21559a957dc4d64e57374933eebb3d398f2d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 20:30:52 +0000 Subject: [PATCH 28/34] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index be9bc18a..e690d820 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell/hyperspell-65556393ee1b73d7aa80b6cdf194830148bd8ca311269345c64e6a14522f68d9.yml -openapi_spec_hash: d71925df7395382b9184ee8d55b99fdf +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell/hyperspell-cff82eeac9caa392a1f8ea392a5ce41729b35768f7eaef96fb198366fae885cd.yml +openapi_spec_hash: 166e3610425ac0de0b51c2bcf3c63596 config_hash: 597eba5e5eaec83a5f0db3d946af8db5 From 335d0288f43e7f681a783f887ce1415ee57cad22 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 19:30:56 +0000 Subject: [PATCH 29/34] feat(api): api update --- .stats.yml | 4 ++-- src/hyperspell/types/shared/resource.py | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index e690d820..3c80b735 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell/hyperspell-cff82eeac9caa392a1f8ea392a5ce41729b35768f7eaef96fb198366fae885cd.yml -openapi_spec_hash: 166e3610425ac0de0b51c2bcf3c63596 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell/hyperspell-0f1b9ea6ae3e53ac49e6a27f2a793b8e0048f53c554f98ded0e3040fe7447621.yml +openapi_spec_hash: f8419914fd519b2fe3e6bdb5f740ad68 config_hash: 597eba5e5eaec83a5f0db3d946af8db5 diff --git a/src/hyperspell/types/shared/resource.py b/src/hyperspell/types/shared/resource.py index 4dfde146..ac7a5c14 100644 --- a/src/hyperspell/types/shared/resource.py +++ b/src/hyperspell/types/shared/resource.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import List, Optional from typing_extensions import Literal from .metadata import Metadata @@ -32,6 +32,14 @@ class Resource(BaseModel): "linear", ] + folder_ancestors: Optional[List[str]] = None + """ + Ordered list of provider folder IDs from immediate parent up to (but not + including) provider root. Used by resolve_sync_mode to walk the actual folder + tree without depending on intermediate policy records. Empty = resource lives at + provider root. + """ + folder_id: Optional[str] = None """Provider folder ID this resource belongs to""" From aa0cc7576de289e8e8dd152c773339e9ac27aafe Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 03:30:51 +0000 Subject: [PATCH 30/34] feat(api): api update --- .stats.yml | 4 ++-- src/hyperspell/resources/memories.py | 22 ++++++++++++++------ src/hyperspell/types/memory_search_params.py | 14 ++++++++----- tests/api_resources/test_memories.py | 4 ++-- 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/.stats.yml b/.stats.yml index 3c80b735..7d27de54 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell/hyperspell-0f1b9ea6ae3e53ac49e6a27f2a793b8e0048f53c554f98ded0e3040fe7447621.yml -openapi_spec_hash: f8419914fd519b2fe3e6bdb5f740ad68 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell/hyperspell-5d5cf14a82b351c9d673a74d87b83f59f000ed244d8e83d9b677663f448cd689.yml +openapi_spec_hash: af0ed94c6c8466bd616c9d4b947130ec config_hash: 597eba5e5eaec83a5f0db3d946af8db5 diff --git a/src/hyperspell/resources/memories.py b/src/hyperspell/resources/memories.py index 24fa2438..da9472ca 100644 --- a/src/hyperspell/resources/memories.py +++ b/src/hyperspell/resources/memories.py @@ -468,7 +468,7 @@ def search( *, query: str, answer: bool | Omit = omit, - effort: int | Omit = omit, + effort: Literal["minimal", "low", "medium", "high"] | Omit = omit, max_results: int | Omit = omit, options: memory_search_params.Options | Omit = omit, sources: List[ @@ -508,8 +508,13 @@ def search( answer: If true, the query will be answered along with matching source documents. - effort: Effort level. 0 = pass query through verbatim. 1 = LLM rewrites the query for - better retrieval and extracts date filters. + effort: How much compute to spend on retrieval. Mirrors the dial popularized by + frontier-model APIs (OpenAI reasoning_effort, etc.). 'minimal' = verbatim + single-shot retrieval (fastest). 'low' = LLM rewrites the query for better + retrieval and extracts date filters. 'medium' = rewrite + agentic refinement + loop (the answer LLM may request additional retrieval rounds, up to 3). 'high' = + rewrite + extended refinement (up to 6 rounds). Higher = better recall, more + latency, more cost. max_results: Maximum number of results to return. @@ -1055,7 +1060,7 @@ async def search( *, query: str, answer: bool | Omit = omit, - effort: int | Omit = omit, + effort: Literal["minimal", "low", "medium", "high"] | Omit = omit, max_results: int | Omit = omit, options: memory_search_params.Options | Omit = omit, sources: List[ @@ -1095,8 +1100,13 @@ async def search( answer: If true, the query will be answered along with matching source documents. - effort: Effort level. 0 = pass query through verbatim. 1 = LLM rewrites the query for - better retrieval and extracts date filters. + effort: How much compute to spend on retrieval. Mirrors the dial popularized by + frontier-model APIs (OpenAI reasoning_effort, etc.). 'minimal' = verbatim + single-shot retrieval (fastest). 'low' = LLM rewrites the query for better + retrieval and extracts date filters. 'medium' = rewrite + agentic refinement + loop (the answer LLM may request additional retrieval rounds, up to 3). 'high' = + rewrite + extended refinement (up to 6 rounds). Higher = better recall, more + latency, more cost. max_results: Maximum number of results to return. diff --git a/src/hyperspell/types/memory_search_params.py b/src/hyperspell/types/memory_search_params.py index 4b98dc25..3e4abd85 100644 --- a/src/hyperspell/types/memory_search_params.py +++ b/src/hyperspell/types/memory_search_params.py @@ -31,11 +31,15 @@ class MemorySearchParams(TypedDict, total=False): answer: bool """If true, the query will be answered along with matching source documents.""" - effort: int - """Effort level. - - 0 = pass query through verbatim. 1 = LLM rewrites the query for better retrieval - and extracts date filters. + effort: Literal["minimal", "low", "medium", "high"] + """How much compute to spend on retrieval. + + Mirrors the dial popularized by frontier-model APIs (OpenAI reasoning_effort, + etc.). 'minimal' = verbatim single-shot retrieval (fastest). 'low' = LLM + rewrites the query for better retrieval and extracts date filters. 'medium' = + rewrite + agentic refinement loop (the answer LLM may request additional + retrieval rounds, up to 3). 'high' = rewrite + extended refinement (up to 6 + rounds). Higher = better recall, more latency, more cost. """ max_results: int diff --git a/tests/api_resources/test_memories.py b/tests/api_resources/test_memories.py index 4d801436..2108b62e 100644 --- a/tests/api_resources/test_memories.py +++ b/tests/api_resources/test_memories.py @@ -292,7 +292,7 @@ def test_method_search_with_all_params(self, client: Hyperspell) -> None: memory = client.memories.search( query="What does Hyperspell do?", answer=True, - effort=0, + effort="minimal", max_results=0, options={ "after": parse_datetime("2019-12-27T18:11:19.117Z"), @@ -703,7 +703,7 @@ async def test_method_search_with_all_params(self, async_client: AsyncHyperspell memory = await async_client.memories.search( query="What does Hyperspell do?", answer=True, - effort=0, + effort="minimal", max_results=0, options={ "after": parse_datetime("2019-12-27T18:11:19.117Z"), From 303776f90abd45d136a630d44d8942f3bb22550e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 17:30:58 +0000 Subject: [PATCH 31/34] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 7d27de54..6ad3fcc5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell/hyperspell-5d5cf14a82b351c9d673a74d87b83f59f000ed244d8e83d9b677663f448cd689.yml -openapi_spec_hash: af0ed94c6c8466bd616c9d4b947130ec +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell/hyperspell-8e3a528aec08c984925789a0e38be318828808754d4f24c45f8df552a81c55c1.yml +openapi_spec_hash: 9a50be5cf5b5eb3bc1203f1ee70a580f config_hash: 597eba5e5eaec83a5f0db3d946af8db5 From cc26c43e007a42b9ac4de6554d0280ef56b2b713 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 18:31:23 +0000 Subject: [PATCH 32/34] feat(api): api update --- .stats.yml | 4 ++-- src/hyperspell/types/connection_list_response.py | 6 ++++++ src/hyperspell/types/integration_list_response.py | 3 +++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 6ad3fcc5..0083ac70 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 30 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell/hyperspell-8e3a528aec08c984925789a0e38be318828808754d4f24c45f8df552a81c55c1.yml -openapi_spec_hash: 9a50be5cf5b5eb3bc1203f1ee70a580f +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell/hyperspell-25f6e417d8ade75d7f759ed0a016931fbc51c01bdb990957d06cd8a5b81bd710.yml +openapi_spec_hash: ddcd074be4feadcc2c35db961e566a32 config_hash: 597eba5e5eaec83a5f0db3d946af8db5 diff --git a/src/hyperspell/types/connection_list_response.py b/src/hyperspell/types/connection_list_response.py index e6523e55..bbf6485e 100644 --- a/src/hyperspell/types/connection_list_response.py +++ b/src/hyperspell/types/connection_list_response.py @@ -39,6 +39,12 @@ class Connection(BaseModel): ] """The connection's provider""" + selected_count: Optional[int] = None + """ + Count of items in user_options.channels (Teams: workspaces selected; 0 means + nothing is being indexed for integrations that require selection). + """ + class ConnectionListResponse(BaseModel): connections: List[Connection] diff --git a/src/hyperspell/types/integration_list_response.py b/src/hyperspell/types/integration_list_response.py index a1e9fd67..b1da7bc5 100644 --- a/src/hyperspell/types/integration_list_response.py +++ b/src/hyperspell/types/integration_list_response.py @@ -48,6 +48,9 @@ class Integration(BaseModel): actions_only: Optional[bool] = None """Whether this integration only supports write actions (no sync)""" + requires_channel_selection: Optional[bool] = None + """Whether the user must select channels before indexing starts""" + class IntegrationListResponse(BaseModel): integrations: List[Integration] From 6dbd3ecf89c76051dde508e759d15db97acc4141 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 9 May 2026 04:06:13 +0000 Subject: [PATCH 33/34] fix(client): add missing f-string prefix in file type error message --- src/hyperspell/_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hyperspell/_files.py b/src/hyperspell/_files.py index 34fa5459..e5146d91 100644 --- a/src/hyperspell/_files.py +++ b/src/hyperspell/_files.py @@ -99,7 +99,7 @@ async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles elif is_sequence_t(files): files = [(key, await _async_transform_file(file)) for key, file in files] else: - raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence") + raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence") return files From e7cfe05f3f6be2f07a86eb7e499f1ce833e436e5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 9 May 2026 04:06:48 +0000 Subject: [PATCH 34/34] release: 0.38.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- src/hyperspell/_version.py | 2 +- 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 51acdaa4..8ea07c9a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.37.0" + ".": "0.38.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 86d769ac..44c380cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ # Changelog +## 0.38.0 (2026-05-09) + +Full Changelog: [v0.37.0...v0.38.0](https://github.com/hyperspell/python-sdk/compare/v0.37.0...v0.38.0) + +### Features + +* **api:** api update ([cc26c43](https://github.com/hyperspell/python-sdk/commit/cc26c43e007a42b9ac4de6554d0280ef56b2b713)) +* **api:** api update ([aa0cc75](https://github.com/hyperspell/python-sdk/commit/aa0cc7576de289e8e8dd152c773339e9ac27aafe)) +* **api:** api update ([335d028](https://github.com/hyperspell/python-sdk/commit/335d0288f43e7f681a783f887ce1415ee57cad22)) +* **api:** api update ([3b25c10](https://github.com/hyperspell/python-sdk/commit/3b25c10a9f2be985dcfbfaf059f5595ea316d5f5)) +* **api:** api update ([92fd43a](https://github.com/hyperspell/python-sdk/commit/92fd43a735c10a678fb2e9b3f052c43607b6fb8a)) +* **api:** api update ([7a8e495](https://github.com/hyperspell/python-sdk/commit/7a8e495ee6b8f162dce5c3124b007d2d60f6cdd6)) +* **api:** manual updates ([23c9221](https://github.com/hyperspell/python-sdk/commit/23c9221674f7d4d4d563b3e8d80d55c0b884114a)) +* support setting headers via env ([eba0398](https://github.com/hyperspell/python-sdk/commit/eba039883cd57a0b9df0c4779d1370af59cd36ce)) + + +### Bug Fixes + +* **client:** add missing f-string prefix in file type error message ([6dbd3ec](https://github.com/hyperspell/python-sdk/commit/6dbd3ecf89c76051dde508e759d15db97acc4141)) +* use correct field name format for multipart file arrays ([0f569cc](https://github.com/hyperspell/python-sdk/commit/0f569cccc70ea96a2da668846b62bf65af449561)) + + +### Performance Improvements + +* **client:** optimize file structure copying in multipart requests ([c28640c](https://github.com/hyperspell/python-sdk/commit/c28640c64d25fd5dde07836e60e970069b0b65fe)) + + +### Chores + +* **internal:** more robust bootstrap script ([d4a39dc](https://github.com/hyperspell/python-sdk/commit/d4a39dc3c6f710bf9b9c481197043eeb4a9f8ac9)) +* **internal:** reformat pyproject.toml ([19a83c3](https://github.com/hyperspell/python-sdk/commit/19a83c32f936735d997727bda0bb0ba523f8086c)) +* **tests:** bump steady to v0.22.1 ([7d67d91](https://github.com/hyperspell/python-sdk/commit/7d67d91802b13e3110af4e1ab313cc36ed66ca74)) + ## 0.37.0 (2026-04-16) Full Changelog: [v0.36.0...v0.37.0](https://github.com/hyperspell/python-sdk/compare/v0.36.0...v0.37.0) diff --git a/pyproject.toml b/pyproject.toml index 9dec6210..8ac832e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "hyperspell" -version = "0.37.0" +version = "0.38.0" description = "The official Python library for the hyperspell API" dynamic = ["readme"] license = "MIT" diff --git a/src/hyperspell/_version.py b/src/hyperspell/_version.py index ea4e056d..db685eed 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.37.0" # x-release-please-version +__version__ = "0.38.0" # x-release-please-version