From 746fed3c17e9a7378b25c25f7bd53dfb61103798 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 22:30:14 +0000 Subject: [PATCH 01/32] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index fa467814..6d0439a8 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-b1f2b7cb843e6f4e6123e838ce29cbbaea0a48b1a72057632de1d0d21727c5d8.yml -openapi_spec_hash: 21a354f587a2fe19797860c7b6da81a9 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-74db7f2f52f21dd91ef3652895501f0a82008f699a5d0b49a58059609624b4dc.yml +openapi_spec_hash: 58616ba29b9ef5d0e0615b766bfd1a93 config_hash: 0ed970a9634b33d0af471738b478740d From b2b3ca2749930db6558bbe5bc3bb1ce4adadfe0e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 03:51:10 +0000 Subject: [PATCH 02/32] fix: sanitize endpoint path params --- src/hyperspell/_utils/__init__.py | 1 + src/hyperspell/_utils/_path.py | 127 ++++++++++++++++++ src/hyperspell/resources/connections.py | 5 +- src/hyperspell/resources/evaluate.py | 14 +- src/hyperspell/resources/folders.py | 26 ++-- .../resources/integrations/integrations.py | 6 +- src/hyperspell/resources/memories.py | 14 +- tests/test_utils/test_path.py | 89 ++++++++++++ 8 files changed, 254 insertions(+), 28 deletions(-) create mode 100644 src/hyperspell/_utils/_path.py create mode 100644 tests/test_utils/test_path.py diff --git a/src/hyperspell/_utils/__init__.py b/src/hyperspell/_utils/__init__.py index dc64e29a..10cb66d2 100644 --- a/src/hyperspell/_utils/__init__.py +++ b/src/hyperspell/_utils/__init__.py @@ -1,3 +1,4 @@ +from ._path import path_template as path_template from ._sync import asyncify as asyncify from ._proxy import LazyProxy as LazyProxy from ._utils import ( diff --git a/src/hyperspell/_utils/_path.py b/src/hyperspell/_utils/_path.py new file mode 100644 index 00000000..4d6e1e4c --- /dev/null +++ b/src/hyperspell/_utils/_path.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +import re +from typing import ( + Any, + Mapping, + Callable, +) +from urllib.parse import quote + +# Matches '.' or '..' where each dot is either literal or percent-encoded (%2e / %2E). +_DOT_SEGMENT_RE = re.compile(r"^(?:\.|%2[eE]){1,2}$") + +_PLACEHOLDER_RE = re.compile(r"\{(\w+)\}") + + +def _quote_path_segment_part(value: str) -> str: + """Percent-encode `value` for use in a URI path segment. + + Considers characters not in `pchar` set from RFC 3986 §3.3 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 + """ + # quote() already treats unreserved characters (letters, digits, and -._~) + # as safe, so we only need to add sub-delims, ':', and '@'. + # Notably, unlike the default `safe` for quote(), / is unsafe and must be quoted. + return quote(value, safe="!$&'()*+,;=:@") + + +def _quote_query_part(value: str) -> str: + """Percent-encode `value` for use in a URI query string. + + Considers &, = and characters not in `query` set from RFC 3986 §3.4 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.4 + """ + return quote(value, safe="!$'()*+,;:@/?") + + +def _quote_fragment_part(value: str) -> str: + """Percent-encode `value` for use in a URI fragment. + + Considers characters not in `fragment` set from RFC 3986 §3.5 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.5 + """ + return quote(value, safe="!$&'()*+,;=:@/?") + + +def _interpolate( + template: str, + values: Mapping[str, Any], + quoter: Callable[[str], str], +) -> str: + """Replace {name} placeholders in `template`, quoting each value with `quoter`. + + Placeholder names are looked up in `values`. + + Raises: + KeyError: If a placeholder is not found in `values`. + """ + # re.split with a capturing group returns alternating + # [text, name, text, name, ..., text] elements. + parts = _PLACEHOLDER_RE.split(template) + + for i in range(1, len(parts), 2): + name = parts[i] + if name not in values: + raise KeyError(f"a value for placeholder {{{name}}} was not provided") + val = values[name] + if val is None: + parts[i] = "null" + elif isinstance(val, bool): + parts[i] = "true" if val else "false" + else: + parts[i] = quoter(str(values[name])) + + return "".join(parts) + + +def path_template(template: str, /, **kwargs: Any) -> str: + """Interpolate {name} placeholders in `template` from keyword arguments. + + Args: + template: The template string containing {name} placeholders. + **kwargs: Keyword arguments to interpolate into the template. + + Returns: + The template with placeholders interpolated and percent-encoded. + + Safe characters for percent-encoding are dependent on the URI component. + Placeholders in path and fragment portions are percent-encoded where the `segment` + and `fragment` sets from RFC 3986 respectively are considered safe. + Placeholders in the query portion are percent-encoded where the `query` set from + RFC 3986 §3.3 is considered safe except for = and & characters. + + Raises: + KeyError: If a placeholder is not found in `kwargs`. + ValueError: If resulting path contains /./ or /../ segments (including percent-encoded dot-segments). + """ + # Split the template into path, query, and fragment portions. + fragment_template: str | None = None + query_template: str | None = None + + rest = template + if "#" in rest: + rest, fragment_template = rest.split("#", 1) + if "?" in rest: + rest, query_template = rest.split("?", 1) + path_template = rest + + # Interpolate each portion with the appropriate quoting rules. + path_result = _interpolate(path_template, kwargs, _quote_path_segment_part) + + # Reject dot-segments (. and ..) in the final assembled path. The check + # runs after interpolation so that adjacent placeholders or a mix of static + # text and placeholders that together form a dot-segment are caught. + # Also reject percent-encoded dot-segments to protect against incorrectly + # implemented normalization in servers/proxies. + for segment in path_result.split("/"): + if _DOT_SEGMENT_RE.match(segment): + raise ValueError(f"Constructed path {path_result!r} contains dot-segment {segment!r} which is not allowed") + + result = path_result + if query_template is not None: + result += "?" + _interpolate(query_template, kwargs, _quote_query_part) + if fragment_template is not None: + result += "#" + _interpolate(fragment_template, kwargs, _quote_fragment_part) + + return result diff --git a/src/hyperspell/resources/connections.py b/src/hyperspell/resources/connections.py index 98799ee3..f266c681 100644 --- a/src/hyperspell/resources/connections.py +++ b/src/hyperspell/resources/connections.py @@ -5,6 +5,7 @@ import httpx from .._types import Body, Query, Headers, NotGiven, not_given +from .._utils import path_template from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -86,7 +87,7 @@ def revoke( if not connection_id: raise ValueError(f"Expected a non-empty value for `connection_id` but received {connection_id!r}") return self._delete( - f"/connections/{connection_id}/revoke", + path_template("/connections/{connection_id}/revoke", connection_id=connection_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -160,7 +161,7 @@ async def revoke( if not connection_id: raise ValueError(f"Expected a non-empty value for `connection_id` but received {connection_id!r}") return await self._delete( - f"/connections/{connection_id}/revoke", + path_template("/connections/{connection_id}/revoke", connection_id=connection_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/hyperspell/resources/evaluate.py b/src/hyperspell/resources/evaluate.py index 58dc4bf3..02b1c4d1 100644 --- a/src/hyperspell/resources/evaluate.py +++ b/src/hyperspell/resources/evaluate.py @@ -8,7 +8,7 @@ from ..types import evaluate_score_query_params, evaluate_score_highlight_params from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -71,7 +71,7 @@ def get_query( if not query_id: raise ValueError(f"Expected a non-empty value for `query_id` but received {query_id!r}") return self._get( - f"/evaluate/query/{query_id}", + path_template("/evaluate/query/{query_id}", query_id=query_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -110,7 +110,7 @@ def score_highlight( if not highlight_id: raise ValueError(f"Expected a non-empty value for `highlight_id` but received {highlight_id!r}") return self._post( - f"/evaluate/highlight/{highlight_id}", + path_template("/evaluate/highlight/{highlight_id}", highlight_id=highlight_id), body=maybe_transform( { "comment": comment, @@ -153,7 +153,7 @@ def score_query( if not query_id: raise ValueError(f"Expected a non-empty value for `query_id` but received {query_id!r}") return self._post( - f"/evaluate/query/{query_id}", + path_template("/evaluate/query/{query_id}", query_id=query_id), body=maybe_transform({"score": score}, evaluate_score_query_params.EvaluateScoreQueryParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout @@ -208,7 +208,7 @@ async def get_query( if not query_id: raise ValueError(f"Expected a non-empty value for `query_id` but received {query_id!r}") return await self._get( - f"/evaluate/query/{query_id}", + path_template("/evaluate/query/{query_id}", query_id=query_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -247,7 +247,7 @@ async def score_highlight( if not highlight_id: raise ValueError(f"Expected a non-empty value for `highlight_id` but received {highlight_id!r}") return await self._post( - f"/evaluate/highlight/{highlight_id}", + path_template("/evaluate/highlight/{highlight_id}", highlight_id=highlight_id), body=await async_maybe_transform( { "comment": comment, @@ -290,7 +290,7 @@ async def score_query( if not query_id: raise ValueError(f"Expected a non-empty value for `query_id` but received {query_id!r}") return await self._post( - f"/evaluate/query/{query_id}", + path_template("/evaluate/query/{query_id}", query_id=query_id), body=await async_maybe_transform({"score": score}, evaluate_score_query_params.EvaluateScoreQueryParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout diff --git a/src/hyperspell/resources/folders.py b/src/hyperspell/resources/folders.py index 9ebf8451..ed341617 100644 --- a/src/hyperspell/resources/folders.py +++ b/src/hyperspell/resources/folders.py @@ -9,7 +9,7 @@ from ..types import folder_list_params, folder_set_policies_params from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -79,7 +79,7 @@ def list( if not connection_id: raise ValueError(f"Expected a non-empty value for `connection_id` but received {connection_id!r}") return self._get( - f"/connections/{connection_id}/folders", + path_template("/connections/{connection_id}/folders", connection_id=connection_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -119,7 +119,11 @@ def delete_policy( if not policy_id: raise ValueError(f"Expected a non-empty value for `policy_id` but received {policy_id!r}") return self._delete( - f"/connections/{connection_id}/folder-policies/{policy_id}", + path_template( + "/connections/{connection_id}/folder-policies/{policy_id}", + connection_id=connection_id, + policy_id=policy_id, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -152,7 +156,7 @@ def list_policies( if not connection_id: raise ValueError(f"Expected a non-empty value for `connection_id` but received {connection_id!r}") return self._get( - f"/connections/{connection_id}/folder-policies", + path_template("/connections/{connection_id}/folder-policies", connection_id=connection_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -200,7 +204,7 @@ def set_policies( if not connection_id: raise ValueError(f"Expected a non-empty value for `connection_id` but received {connection_id!r}") return self._post( - f"/connections/{connection_id}/folder-policies", + path_template("/connections/{connection_id}/folder-policies", connection_id=connection_id), body=maybe_transform( { "provider_folder_id": provider_folder_id, @@ -270,7 +274,7 @@ async def list( if not connection_id: raise ValueError(f"Expected a non-empty value for `connection_id` but received {connection_id!r}") return await self._get( - f"/connections/{connection_id}/folders", + path_template("/connections/{connection_id}/folders", connection_id=connection_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -310,7 +314,11 @@ async def delete_policy( if not policy_id: raise ValueError(f"Expected a non-empty value for `policy_id` but received {policy_id!r}") return await self._delete( - f"/connections/{connection_id}/folder-policies/{policy_id}", + path_template( + "/connections/{connection_id}/folder-policies/{policy_id}", + connection_id=connection_id, + policy_id=policy_id, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -343,7 +351,7 @@ async def list_policies( if not connection_id: raise ValueError(f"Expected a non-empty value for `connection_id` but received {connection_id!r}") return await self._get( - f"/connections/{connection_id}/folder-policies", + path_template("/connections/{connection_id}/folder-policies", connection_id=connection_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -391,7 +399,7 @@ async def set_policies( if not connection_id: raise ValueError(f"Expected a non-empty value for `connection_id` but received {connection_id!r}") return await self._post( - f"/connections/{connection_id}/folder-policies", + path_template("/connections/{connection_id}/folder-policies", connection_id=connection_id), body=await async_maybe_transform( { "provider_folder_id": provider_folder_id, diff --git a/src/hyperspell/resources/integrations/integrations.py b/src/hyperspell/resources/integrations/integrations.py index 09eb99a0..72f1d33d 100644 --- a/src/hyperspell/resources/integrations/integrations.py +++ b/src/hyperspell/resources/integrations/integrations.py @@ -16,7 +16,7 @@ ) from ...types import integration_connect_params from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -126,7 +126,7 @@ def connect( if not integration_id: raise ValueError(f"Expected a non-empty value for `integration_id` but received {integration_id!r}") return self._get( - f"/integrations/{integration_id}/connect", + path_template("/integrations/{integration_id}/connect", integration_id=integration_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -218,7 +218,7 @@ async def connect( if not integration_id: raise ValueError(f"Expected a non-empty value for `integration_id` but received {integration_id!r}") return await self._get( - f"/integrations/{integration_id}/connect", + path_template("/integrations/{integration_id}/connect", integration_id=integration_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, diff --git a/src/hyperspell/resources/memories.py b/src/hyperspell/resources/memories.py index 83b15da8..92ad50cc 100644 --- a/src/hyperspell/resources/memories.py +++ b/src/hyperspell/resources/memories.py @@ -17,7 +17,7 @@ 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 +from .._utils import extract_files, path_template, maybe_transform, deepcopy_minimal, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -122,7 +122,7 @@ def update( if not resource_id: raise ValueError(f"Expected a non-empty value for `resource_id` but received {resource_id!r}") return self._post( - f"/memories/update/{source}/{resource_id}", + path_template("/memories/update/{source}/{resource_id}", source=source, resource_id=resource_id), body=maybe_transform( { "collection": collection, @@ -276,7 +276,7 @@ def delete( if not resource_id: raise ValueError(f"Expected a non-empty value for `resource_id` but received {resource_id!r}") return self._delete( - f"/memories/delete/{source}/{resource_id}", + path_template("/memories/delete/{source}/{resource_id}", source=source, resource_id=resource_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -435,7 +435,7 @@ def get( if not resource_id: raise ValueError(f"Expected a non-empty value for `resource_id` but received {resource_id!r}") return self._get( - f"/memories/get/{source}/{resource_id}", + path_template("/memories/get/{source}/{resource_id}", source=source, resource_id=resource_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -679,7 +679,7 @@ async def update( if not resource_id: raise ValueError(f"Expected a non-empty value for `resource_id` but received {resource_id!r}") return await self._post( - f"/memories/update/{source}/{resource_id}", + path_template("/memories/update/{source}/{resource_id}", source=source, resource_id=resource_id), body=await async_maybe_transform( { "collection": collection, @@ -833,7 +833,7 @@ async def delete( if not resource_id: raise ValueError(f"Expected a non-empty value for `resource_id` but received {resource_id!r}") return await self._delete( - f"/memories/delete/{source}/{resource_id}", + path_template("/memories/delete/{source}/{resource_id}", source=source, resource_id=resource_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -992,7 +992,7 @@ async def get( if not resource_id: raise ValueError(f"Expected a non-empty value for `resource_id` but received {resource_id!r}") return await self._get( - f"/memories/get/{source}/{resource_id}", + path_template("/memories/get/{source}/{resource_id}", source=source, resource_id=resource_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/tests/test_utils/test_path.py b/tests/test_utils/test_path.py new file mode 100644 index 00000000..b6269140 --- /dev/null +++ b/tests/test_utils/test_path.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from typing import Any + +import pytest + +from hyperspell._utils._path import path_template + + +@pytest.mark.parametrize( + "template, kwargs, expected", + [ + ("/v1/{id}", dict(id="abc"), "/v1/abc"), + ("/v1/{a}/{b}", dict(a="x", b="y"), "/v1/x/y"), + ("/v1/{a}{b}/path/{c}?val={d}#{e}", dict(a="x", b="y", c="z", d="u", e="v"), "/v1/xy/path/z?val=u#v"), + ("/{w}/{w}", dict(w="echo"), "/echo/echo"), + ("/v1/static", {}, "/v1/static"), + ("", {}, ""), + ("/v1/?q={n}&count=10", dict(n=42), "/v1/?q=42&count=10"), + ("/v1/{v}", dict(v=None), "/v1/null"), + ("/v1/{v}", dict(v=True), "/v1/true"), + ("/v1/{v}", dict(v=False), "/v1/false"), + ("/v1/{v}", dict(v=".hidden"), "/v1/.hidden"), # dot prefix ok + ("/v1/{v}", dict(v="file.txt"), "/v1/file.txt"), # dot in middle ok + ("/v1/{v}", dict(v="..."), "/v1/..."), # triple dot ok + ("/v1/{a}{b}", dict(a=".", b="txt"), "/v1/.txt"), # dot var combining with adjacent to be ok + ("/items?q={v}#{f}", dict(v=".", f=".."), "/items?q=.#.."), # dots in query/fragment are fine + ( + "/v1/{a}?query={b}", + dict(a="../../other/endpoint", b="a&bad=true"), + "/v1/..%2F..%2Fother%2Fendpoint?query=a%26bad%3Dtrue", + ), + ("/v1/{val}", dict(val="a/b/c"), "/v1/a%2Fb%2Fc"), + ("/v1/{val}", dict(val="a/b/c?query=value"), "/v1/a%2Fb%2Fc%3Fquery=value"), + ("/v1/{val}", dict(val="a/b/c?query=value&bad=true"), "/v1/a%2Fb%2Fc%3Fquery=value&bad=true"), + ("/v1/{val}", dict(val="%20"), "/v1/%2520"), # escapes escape sequences in input + # Query: slash and ? are safe, # is not + ("/items?q={v}", dict(v="a/b"), "/items?q=a/b"), + ("/items?q={v}", dict(v="a?b"), "/items?q=a?b"), + ("/items?q={v}", dict(v="a#b"), "/items?q=a%23b"), + ("/items?q={v}", dict(v="a b"), "/items?q=a%20b"), + # Fragment: slash and ? are safe + ("/docs#{v}", dict(v="a/b"), "/docs#a/b"), + ("/docs#{v}", dict(v="a?b"), "/docs#a?b"), + # Path: slash, ? and # are all encoded + ("/v1/{v}", dict(v="a/b"), "/v1/a%2Fb"), + ("/v1/{v}", dict(v="a?b"), "/v1/a%3Fb"), + ("/v1/{v}", dict(v="a#b"), "/v1/a%23b"), + # same var encoded differently by component + ( + "/v1/{v}?q={v}#{v}", + dict(v="a/b?c#d"), + "/v1/a%2Fb%3Fc%23d?q=a/b?c%23d#a/b?c%23d", + ), + ("/v1/{val}", dict(val="x?admin=true"), "/v1/x%3Fadmin=true"), # query injection + ("/v1/{val}", dict(val="x#admin"), "/v1/x%23admin"), # fragment injection + ], +) +def test_interpolation(template: str, kwargs: dict[str, Any], expected: str) -> None: + assert path_template(template, **kwargs) == expected + + +def test_missing_kwarg_raises_key_error() -> None: + with pytest.raises(KeyError, match="org_id"): + path_template("/v1/{org_id}") + + +@pytest.mark.parametrize( + "template, kwargs", + [ + ("{a}/path", dict(a=".")), + ("{a}/path", dict(a="..")), + ("/v1/{a}", dict(a=".")), + ("/v1/{a}", dict(a="..")), + ("/v1/{a}/path", dict(a=".")), + ("/v1/{a}/path", dict(a="..")), + ("/v1/{a}{b}", dict(a=".", b=".")), # adjacent vars → ".." + ("/v1/{a}.", dict(a=".")), # var + static → ".." + ("/v1/{a}{b}", dict(a="", b=".")), # empty + dot → "." + ("/v1/%2e/{x}", dict(x="ok")), # encoded dot in static text + ("/v1/%2e./{x}", dict(x="ok")), # mixed encoded ".." in static + ("/v1/.%2E/{x}", dict(x="ok")), # mixed encoded ".." in static + ("/v1/{v}?q=1", dict(v="..")), + ("/v1/{v}#frag", dict(v="..")), + ], +) +def test_dot_segment_rejected(template: str, kwargs: dict[str, Any]) -> None: + with pytest.raises(ValueError, match="dot-segment"): + path_template(template, **kwargs) From 441e854a4933f7abd07961f08a58d7f7b7b3ea60 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 03:52:41 +0000 Subject: [PATCH 03/32] refactor(tests): switch from prism to steady --- CONTRIBUTING.md | 2 +- scripts/mock | 26 +++++++++++++------------- scripts/test | 16 ++++++++-------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a02e59a5..23e72985 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,7 +85,7 @@ $ pip install ./path-to-wheel-file.whl ## Running tests -Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. +Most tests require you to [set up a mock server](https://github.com/dgellow/steady) against the OpenAPI spec to run the tests. ```sh $ ./scripts/mock diff --git a/scripts/mock b/scripts/mock index bcf3b392..38201de8 100755 --- a/scripts/mock +++ b/scripts/mock @@ -19,34 +19,34 @@ fi echo "==> Starting mock server with URL ${URL}" -# Run prism mock on the given spec +# 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=@stainless-api/prism-cli@5.15.0 -- prism --version + npm exec --package=@stdy/cli@0.19.3 -- steady --version - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & + npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-query-object-format=brackets "$URL" &> .stdy.log & - # Wait for server to come online (max 30s) + # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" attempts=0 - while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do + while ! curl --silent --fail "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1; do + if ! kill -0 $! 2>/dev/null; then + echo + cat .stdy.log + exit 1 + fi attempts=$((attempts + 1)) if [ "$attempts" -ge 300 ]; then echo - echo "Timed out waiting for Prism server to start" - cat .prism.log + echo "Timed out waiting for Steady server to start" + cat .stdy.log exit 1 fi echo -n "." sleep 0.1 done - if grep -q "✖ fatal" ".prism.log"; then - cat .prism.log - exit 1 - fi - echo else - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" + npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index b56970b7..b9eec014 100755 --- a/scripts/test +++ b/scripts/test @@ -9,8 +9,8 @@ GREEN='\033[0;32m' YELLOW='\033[0;33m' NC='\033[0m' # No Color -function prism_is_running() { - curl --silent "http://localhost:4010" >/dev/null 2>&1 +function steady_is_running() { + curl --silent "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1 } kill_server_on_port() { @@ -25,7 +25,7 @@ function is_overriding_api_base_url() { [ -n "$TEST_API_BASE_URL" ] } -if ! is_overriding_api_base_url && ! prism_is_running ; then +if ! is_overriding_api_base_url && ! steady_is_running ; then # When we exit this script, make sure to kill the background mock server process trap 'kill_server_on_port 4010' EXIT @@ -36,19 +36,19 @@ fi if is_overriding_api_base_url ; then echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" echo -elif ! prism_is_running ; then - echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" +elif ! steady_is_running ; then + echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Steady server" echo -e "running against your OpenAPI spec." echo echo -e "To run the server, pass in the path or url of your OpenAPI" - echo -e "spec to the prism command:" + echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.3 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-query-object-format=brackets${NC}" echo exit 1 else - echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" + echo -e "${GREEN}✔ Mock steady server is running with your OpenAPI spec${NC}" echo fi From a855b0a42e5926bdb4c1ece32f74370f9b481241 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 05:08:28 +0000 Subject: [PATCH 04/32] chore(tests): bump steady to v0.19.4 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 38201de8..e1c19e88 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.19.3 -- steady --version + npm exec --package=@stdy/cli@0.19.4 -- steady --version - npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-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.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index b9eec014..2abf1b60 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.19.3 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.4 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 From 7ea97c0c00ee4d81301a6b2214ed677e88a5fcd8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 05:13:59 +0000 Subject: [PATCH 05/32] chore(tests): bump steady to v0.19.5 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index e1c19e88..ab814d38 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.19.4 -- steady --version + npm exec --package=@stdy/cli@0.19.5 -- steady --version - npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-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.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 2abf1b60..5105f92e 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.19.4 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.5 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 From 618090275519b615d2f0b4679aecb9a86ccbab0b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 21:30:11 +0000 Subject: [PATCH 06/32] feat(api): api update --- .stats.yml | 4 ++-- src/hyperspell/resources/actions.py | 4 ++++ src/hyperspell/resources/memories.py | 10 ++++++++++ src/hyperspell/types/action_add_reaction_params.py | 1 + src/hyperspell/types/action_send_message_params.py | 1 + src/hyperspell/types/auth_me_response.py | 2 ++ src/hyperspell/types/connection_list_response.py | 1 + src/hyperspell/types/integration_list_response.py | 1 + .../types/integrations/web_crawler_index_response.py | 1 + src/hyperspell/types/memory.py | 1 + src/hyperspell/types/memory_delete_response.py | 1 + src/hyperspell/types/memory_list_params.py | 1 + src/hyperspell/types/memory_search_params.py | 1 + src/hyperspell/types/memory_status.py | 1 + src/hyperspell/types/memory_update_params.py | 1 + src/hyperspell/types/shared/resource.py | 1 + 16 files changed, 30 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 6d0439a8..58f19c0f 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-74db7f2f52f21dd91ef3652895501f0a82008f699a5d0b49a58059609624b4dc.yml -openapi_spec_hash: 58616ba29b9ef5d0e0615b766bfd1a93 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-f36a4766adbf62d457ea756c663e49bca3b0aaea3690a4985d546806850c2e88.yml +openapi_spec_hash: 5b623572e06fe5afbd5291a90b89e51d config_hash: 0ed970a9634b33d0af471738b478740d diff --git a/src/hyperspell/resources/actions.py b/src/hyperspell/resources/actions.py index d2a85593..40aec472 100644 --- a/src/hyperspell/resources/actions.py +++ b/src/hyperspell/resources/actions.py @@ -64,6 +64,7 @@ def add_reaction( "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ], timestamp: str, connection: Optional[str] | Omit = omit, @@ -131,6 +132,7 @@ def send_message( "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ], text: str, channel: Optional[str] | Omit = omit, @@ -223,6 +225,7 @@ async def add_reaction( "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ], timestamp: str, connection: Optional[str] | Omit = omit, @@ -290,6 +293,7 @@ async def send_message( "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ], text: str, channel: Optional[str] | Omit = omit, diff --git a/src/hyperspell/resources/memories.py b/src/hyperspell/resources/memories.py index 92ad50cc..b0b2f163 100644 --- a/src/hyperspell/resources/memories.py +++ b/src/hyperspell/resources/memories.py @@ -77,6 +77,7 @@ def update( "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ], collection: Union[str, object, None] | Omit = omit, metadata: Union[Dict[str, Union[str, float, bool, None]], object, None] | Omit = omit, @@ -160,6 +161,7 @@ def list( "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ] ] | Omit = omit, @@ -237,6 +239,7 @@ def delete( "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ], # 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. @@ -410,6 +413,7 @@ def get( "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ], # 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. @@ -464,6 +468,7 @@ def search( "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ] ] | Omit = omit, @@ -634,6 +639,7 @@ async def update( "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ], collection: Union[str, object, None] | Omit = omit, metadata: Union[Dict[str, Union[str, float, bool, None]], object, None] | Omit = omit, @@ -717,6 +723,7 @@ def list( "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ] ] | Omit = omit, @@ -794,6 +801,7 @@ async def delete( "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ], # 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. @@ -967,6 +975,7 @@ async def get( "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ], # 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. @@ -1021,6 +1030,7 @@ async def search( "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ] ] | Omit = omit, diff --git a/src/hyperspell/types/action_add_reaction_params.py b/src/hyperspell/types/action_add_reaction_params.py index 21c4e18c..98177146 100644 --- a/src/hyperspell/types/action_add_reaction_params.py +++ b/src/hyperspell/types/action_add_reaction_params.py @@ -30,6 +30,7 @@ class ActionAddReactionParams(TypedDict, total=False): "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ] ] """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 8c62ade5..70d2ddbd 100644 --- a/src/hyperspell/types/action_send_message_params.py +++ b/src/hyperspell/types/action_send_message_params.py @@ -24,6 +24,7 @@ class ActionSendMessageParams(TypedDict, total=False): "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ] ] """Integration provider (e.g., slack)""" diff --git a/src/hyperspell/types/auth_me_response.py b/src/hyperspell/types/auth_me_response.py index cdff44b7..46e0e2a3 100644 --- a/src/hyperspell/types/auth_me_response.py +++ b/src/hyperspell/types/auth_me_response.py @@ -47,6 +47,7 @@ class AuthMeResponse(BaseModel): "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ] ] """All integrations available for the app""" @@ -66,6 +67,7 @@ class AuthMeResponse(BaseModel): "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ] ] """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 15f8f5f5..ac6e71c2 100644 --- a/src/hyperspell/types/connection_list_response.py +++ b/src/hyperspell/types/connection_list_response.py @@ -32,6 +32,7 @@ class Connection(BaseModel): "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ] """The connection's provider""" diff --git a/src/hyperspell/types/integration_list_response.py b/src/hyperspell/types/integration_list_response.py index 98e07e93..febd013d 100644 --- a/src/hyperspell/types/integration_list_response.py +++ b/src/hyperspell/types/integration_list_response.py @@ -38,6 +38,7 @@ class Integration(BaseModel): "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ] """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 e1fd856d..bcbe49cc 100644 --- a/src/hyperspell/types/integrations/web_crawler_index_response.py +++ b/src/hyperspell/types/integrations/web_crawler_index_response.py @@ -24,6 +24,7 @@ class WebCrawlerIndexResponse(BaseModel): "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ] status: Literal["pending", "processing", "completed", "failed", "pending_review", "skipped"] diff --git a/src/hyperspell/types/memory.py b/src/hyperspell/types/memory.py index aa0dc03b..e1aaa3f9 100644 --- a/src/hyperspell/types/memory.py +++ b/src/hyperspell/types/memory.py @@ -30,6 +30,7 @@ class Memory(BaseModel): "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ] type: str diff --git a/src/hyperspell/types/memory_delete_response.py b/src/hyperspell/types/memory_delete_response.py index f7a175bd..05cb1d63 100644 --- a/src/hyperspell/types/memory_delete_response.py +++ b/src/hyperspell/types/memory_delete_response.py @@ -28,6 +28,7 @@ class MemoryDeleteResponse(BaseModel): "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ] success: bool diff --git a/src/hyperspell/types/memory_list_params.py b/src/hyperspell/types/memory_list_params.py index 7b468312..ec2a9044 100644 --- a/src/hyperspell/types/memory_list_params.py +++ b/src/hyperspell/types/memory_list_params.py @@ -37,6 +37,7 @@ class MemoryListParams(TypedDict, total=False): "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ] ] """Filter documents by source.""" diff --git a/src/hyperspell/types/memory_search_params.py b/src/hyperspell/types/memory_search_params.py index 6d045db0..5768431b 100644 --- a/src/hyperspell/types/memory_search_params.py +++ b/src/hyperspell/types/memory_search_params.py @@ -52,6 +52,7 @@ class MemorySearchParams(TypedDict, total=False): "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ] ] """Only query documents from these sources.""" diff --git a/src/hyperspell/types/memory_status.py b/src/hyperspell/types/memory_status.py index bf16ab62..fc50b4f5 100644 --- a/src/hyperspell/types/memory_status.py +++ b/src/hyperspell/types/memory_status.py @@ -24,6 +24,7 @@ class MemoryStatus(BaseModel): "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ] 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 a1566dcc..317e419e 100644 --- a/src/hyperspell/types/memory_update_params.py +++ b/src/hyperspell/types/memory_update_params.py @@ -24,6 +24,7 @@ class MemoryUpdateParams(TypedDict, total=False): "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ] ] diff --git a/src/hyperspell/types/shared/resource.py b/src/hyperspell/types/shared/resource.py index d9118fb3..2d5b5472 100644 --- a/src/hyperspell/types/shared/resource.py +++ b/src/hyperspell/types/shared/resource.py @@ -26,6 +26,7 @@ class Resource(BaseModel): "web_crawler", "trace", "microsoft_teams", + "gmail_actions", ] folder_id: Optional[str] = None From d3e77c368350f9a22469db6574d3a44159a043c9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 22:29:51 +0000 Subject: [PATCH 07/32] feat(api): api update --- .stats.yml | 4 ++-- src/hyperspell/types/integration_list_response.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 58f19c0f..60bd61e5 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-f36a4766adbf62d457ea756c663e49bca3b0aaea3690a4985d546806850c2e88.yml -openapi_spec_hash: 5b623572e06fe5afbd5291a90b89e51d +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-23bd01d736c9d8b6a5826bd5e57b051769446e361fa288294175b9f7b0c0abd2.yml +openapi_spec_hash: 2e4983fd11a050ada444d916abdba2db config_hash: 0ed970a9634b33d0af471738b478740d diff --git a/src/hyperspell/types/integration_list_response.py b/src/hyperspell/types/integration_list_response.py index febd013d..9a62cf8b 100644 --- a/src/hyperspell/types/integration_list_response.py +++ b/src/hyperspell/types/integration_list_response.py @@ -42,6 +42,9 @@ class Integration(BaseModel): ] """The integration's provider""" + actions_only: Optional[bool] = None + """Whether this integration only supports write actions (no sync)""" + class IntegrationListResponse(BaseModel): integrations: List[Integration] From 8f27c46407285a62b885632d2bb3c6059ec91eb2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 05:10:36 +0000 Subject: [PATCH 08/32] chore(internal): update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 95ceb189..3824f4c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .prism.log +.stdy.log _dev __pycache__ From 1ecd0bfd11ca3de3436524145c5df5c20b2ec2eb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 05:16:51 +0000 Subject: [PATCH 09/32] chore(tests): bump steady to v0.19.6 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index ab814d38..b319bdfb 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.19.5 -- steady --version + npm exec --package=@stdy/cli@0.19.6 -- steady --version - npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-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.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 5105f92e..ba2580ab 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.19.5 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.6 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 From a967ee56a3e3352819b0502aff9b189e9cd3d419 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 17:30:07 +0000 Subject: [PATCH 10/32] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 60bd61e5..4f7805b6 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-23bd01d736c9d8b6a5826bd5e57b051769446e361fa288294175b9f7b0c0abd2.yml -openapi_spec_hash: 2e4983fd11a050ada444d916abdba2db +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-8b0df513ca50dcaf708a82dec12d9cbbf4ac615203208373ea9b5394add77b17.yml +openapi_spec_hash: 3514190bfcbb5f804d631c0fd3bc0505 config_hash: 0ed970a9634b33d0af471738b478740d From b5a6b2d8467b72cc0bfa192cf2bfd2966de6190e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 03:28:28 +0000 Subject: [PATCH 11/32] chore(ci): skip lint on metadata-only changes Note that we still want to run tests, as these depend on the metadata. --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f29a9d7c..0185f06c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: timeout-minutes: 10 name: lint 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 + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - uses: actions/checkout@v6 @@ -35,7 +35,7 @@ jobs: run: ./scripts/lint build: - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') timeout-minutes: 10 name: build permissions: From 466a8145d00c3810bd1d83ac8531c719377100b8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 03:28:58 +0000 Subject: [PATCH 12/32] chore(tests): bump steady to v0.19.7 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index b319bdfb..09eb49f6 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.19.6 -- steady --version + npm exec --package=@stdy/cli@0.19.7 -- steady --version - npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-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.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index ba2580ab..d13602ae 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.19.6 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 From a0f47209c51eb35cf31974bc158cf9e4df29c0ec Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:29:59 +0000 Subject: [PATCH 13/32] feat(api): api update --- .stats.yml | 4 ++-- src/hyperspell/resources/memories.py | 10 ++++++++++ src/hyperspell/types/memory_search_params.py | 7 +++++++ tests/api_resources/test_memories.py | 2 ++ 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4f7805b6..7824b9c9 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-8b0df513ca50dcaf708a82dec12d9cbbf4ac615203208373ea9b5394add77b17.yml -openapi_spec_hash: 3514190bfcbb5f804d631c0fd3bc0505 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-1cb26e44dcabc61d707d60021634ddc0f49801a4df53e9d0a5b1a94c5cc7fdda.yml +openapi_spec_hash: d79eaf4567192a98df6af149efe3dc86 config_hash: 0ed970a9634b33d0af471738b478740d diff --git a/src/hyperspell/resources/memories.py b/src/hyperspell/resources/memories.py index b0b2f163..9c34885c 100644 --- a/src/hyperspell/resources/memories.py +++ b/src/hyperspell/resources/memories.py @@ -451,6 +451,7 @@ def search( *, query: str, answer: bool | Omit = omit, + effort: int | Omit = omit, max_results: int | Omit = omit, options: memory_search_params.Options | Omit = omit, sources: List[ @@ -487,6 +488,9 @@ 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. + max_results: Maximum number of results to return. options: Search options for the query. @@ -507,6 +511,7 @@ def search( { "query": query, "answer": answer, + "effort": effort, "max_results": max_results, "options": options, "sources": sources, @@ -1013,6 +1018,7 @@ async def search( *, query: str, answer: bool | Omit = omit, + effort: int | Omit = omit, max_results: int | Omit = omit, options: memory_search_params.Options | Omit = omit, sources: List[ @@ -1049,6 +1055,9 @@ 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. + max_results: Maximum number of results to return. options: Search options for the query. @@ -1069,6 +1078,7 @@ async def search( { "query": query, "answer": answer, + "effort": effort, "max_results": max_results, "options": options, "sources": sources, diff --git a/src/hyperspell/types/memory_search_params.py b/src/hyperspell/types/memory_search_params.py index 5768431b..9177df57 100644 --- a/src/hyperspell/types/memory_search_params.py +++ b/src/hyperspell/types/memory_search_params.py @@ -31,6 +31,13 @@ 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. + """ + max_results: int """Maximum number of results to return.""" diff --git a/tests/api_resources/test_memories.py b/tests/api_resources/test_memories.py index f9e04583..1b5b4c87 100644 --- a/tests/api_resources/test_memories.py +++ b/tests/api_resources/test_memories.py @@ -287,6 +287,7 @@ def test_method_search_with_all_params(self, client: Hyperspell) -> None: memory = client.memories.search( query="query", answer=True, + effort=0, max_results=0, options={ "after": parse_datetime("2019-12-27T18:11:19.117Z"), @@ -691,6 +692,7 @@ async def test_method_search_with_all_params(self, async_client: AsyncHyperspell memory = await async_client.memories.search( query="query", answer=True, + effort=0, max_results=0, options={ "after": parse_datetime("2019-12-27T18:11:19.117Z"), From 0e1fd91815afe92b4b7ba73f96e196aa71d5f8c5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 06:41:10 +0000 Subject: [PATCH 14/32] feat(internal): implement indices array format for query and form serialization --- scripts/mock | 4 ++-- scripts/test | 2 +- src/hyperspell/_qs.py | 5 ++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 09eb49f6..290e21b9 100755 --- a/scripts/mock +++ b/scripts/mock @@ -24,7 +24,7 @@ if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout npm exec --package=@stdy/cli@0.19.7 -- steady --version - npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.7 -- 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.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.7 -- 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 d13602ae..e728ca42 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.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=comma --validator-query-array-format=comma --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- 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 diff --git a/src/hyperspell/_qs.py b/src/hyperspell/_qs.py index ada6fd3f..de8c99bc 100644 --- a/src/hyperspell/_qs.py +++ b/src/hyperspell/_qs.py @@ -101,7 +101,10 @@ def _stringify_item( items.extend(self._stringify_item(key, item, opts)) return items elif array_format == "indices": - raise NotImplementedError("The array indices format is not supported yet") + items = [] + for i, item in enumerate(value): + items.extend(self._stringify_item(f"{key}[{i}]", item, opts)) + return items elif array_format == "brackets": items = [] key = key + "[]" From 13e068243400026b7c8e11b11fdca6ba89a2460e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 29 Mar 2026 09:30:05 +0000 Subject: [PATCH 15/32] feat(api): api update --- .stats.yml | 4 ++-- src/hyperspell/resources/sessions.py | 4 ++-- src/hyperspell/types/memory_search_params.py | 2 +- src/hyperspell/types/session_add_params.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.stats.yml b/.stats.yml index 7824b9c9..1c0967c8 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-1cb26e44dcabc61d707d60021634ddc0f49801a4df53e9d0a5b1a94c5cc7fdda.yml -openapi_spec_hash: d79eaf4567192a98df6af149efe3dc86 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-c3c0525ba688c7a7ce78190c7f35bf9f4c03a97863e1c58b24fc4bb751cf2c06.yml +openapi_spec_hash: be38987f64115e3cff6930749bfc6464 config_hash: 0ed970a9634b33d0af471738b478740d diff --git a/src/hyperspell/resources/sessions.py b/src/hyperspell/resources/sessions.py index 9a0ae93c..f99e5830 100644 --- a/src/hyperspell/resources/sessions.py +++ b/src/hyperspell/resources/sessions.py @@ -50,7 +50,7 @@ def add( *, history: str, date: Union[str, datetime] | Omit = omit, - extract: List[Literal["procedure", "memory"]] | Omit = omit, + extract: List[Literal["procedure", "memory", "mood"]] | Omit = omit, format: Optional[Literal["vercel", "hyperdoc", "openclaw"]] | Omit = omit, metadata: Optional[Dict[str, Union[str, float, bool]]] | Omit = omit, session_id: str | Omit = omit, @@ -161,7 +161,7 @@ async def add( *, history: str, date: Union[str, datetime] | Omit = omit, - extract: List[Literal["procedure", "memory"]] | Omit = omit, + extract: List[Literal["procedure", "memory", "mood"]] | Omit = omit, format: Optional[Literal["vercel", "hyperdoc", "openclaw"]] | Omit = omit, metadata: Optional[Dict[str, Union[str, float, bool]]] | Omit = omit, session_id: str | Omit = omit, diff --git a/src/hyperspell/types/memory_search_params.py b/src/hyperspell/types/memory_search_params.py index 9177df57..f220206f 100644 --- a/src/hyperspell/types/memory_search_params.py +++ b/src/hyperspell/types/memory_search_params.py @@ -265,7 +265,7 @@ class Options(TypedDict, total=False): max_results: int """Maximum number of results to return.""" - memory_types: List[Literal["procedure", "memory"]] + memory_types: List[Literal["procedure", "memory", "mood"]] """Filter by memory type. Defaults to generic memories only. Pass multiple types to include procedures, diff --git a/src/hyperspell/types/session_add_params.py b/src/hyperspell/types/session_add_params.py index 0deaa2c0..136c2b88 100644 --- a/src/hyperspell/types/session_add_params.py +++ b/src/hyperspell/types/session_add_params.py @@ -22,7 +22,7 @@ class SessionAddParams(TypedDict, total=False): date: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] """Date of the trace""" - extract: List[Literal["procedure", "memory"]] + extract: List[Literal["procedure", "memory", "mood"]] """What kind of memories to extract from the trace""" format: Optional[Literal["vercel", "hyperdoc", "openclaw"]] From b74eaf31b2933f055c51f8e41981ba061e4ef118 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 23:30:16 +0000 Subject: [PATCH 16/32] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 1c0967c8..ec1ac122 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-c3c0525ba688c7a7ce78190c7f35bf9f4c03a97863e1c58b24fc4bb751cf2c06.yml -openapi_spec_hash: be38987f64115e3cff6930749bfc6464 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-57c0518ddea80a3ee64f4b26b8bf424b4e8ee3afafb9e0e64d0cd6c40cc54fed.yml +openapi_spec_hash: 6f28af0e6529603fec59d3907e71a5c4 config_hash: 0ed970a9634b33d0af471738b478740d From c084e1e3bd7560e139f508e2108744cb1a5fd583 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 18:30:32 +0000 Subject: [PATCH 17/32] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index ec1ac122..98ec08f8 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-57c0518ddea80a3ee64f4b26b8bf424b4e8ee3afafb9e0e64d0cd6c40cc54fed.yml -openapi_spec_hash: 6f28af0e6529603fec59d3907e71a5c4 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-7d29d0843a52840291678a3c6d136f496ae1f956853abaa5003c1284ca2c94aa.yml +openapi_spec_hash: 010597ad0ec6376fbf2f01ec062787ad config_hash: 0ed970a9634b33d0af471738b478740d From 74c3b3b7867b0fef233c314c776c5d867f66937a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 08:55:27 +0000 Subject: [PATCH 18/32] chore(tests): bump steady to v0.20.1 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 290e21b9..15c29941 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.19.7 -- steady --version + npm exec --package=@stdy/cli@0.20.1 -- steady --version - npm exec --package=@stdy/cli@0.19.7 -- 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.20.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.19.7 -- 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.20.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 e728ca42..3ea2a387 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.19.7 -- 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.20.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 3eb71a2ea44943f9b2e10073292d7fd4d58c1b43 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 08:58:44 +0000 Subject: [PATCH 19/32] chore(tests): bump steady to v0.20.2 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 15c29941..5cd7c157 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.1 -- steady --version + npm exec --package=@stdy/cli@0.20.2 -- steady --version - npm exec --package=@stdy/cli@0.20.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 & + 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 & # 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.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" + 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" fi diff --git a/scripts/test b/scripts/test index 3ea2a387..b754adab 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.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 -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 exit 1 From a88e254cd30152452885e1287287ea7d0ee5d134 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 18:23:18 +0000 Subject: [PATCH 20/32] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 98ec08f8..668aa6ab 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-7d29d0843a52840291678a3c6d136f496ae1f956853abaa5003c1284ca2c94aa.yml openapi_spec_hash: 010597ad0ec6376fbf2f01ec062787ad -config_hash: 0ed970a9634b33d0af471738b478740d +config_hash: d94af75a186d3453cbfc0cbf007932c7 From 5af21db84ebfd91d2059606184868b941619cdee Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 18:27:02 +0000 Subject: [PATCH 21/32] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 668aa6ab..3c5ae04a 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-7d29d0843a52840291678a3c6d136f496ae1f956853abaa5003c1284ca2c94aa.yml openapi_spec_hash: 010597ad0ec6376fbf2f01ec062787ad -config_hash: d94af75a186d3453cbfc0cbf007932c7 +config_hash: ce9765da7e780e630590c72a873f5482 From 835372c1584549f06bc3e3f034830b10de2ae962 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 18:27:17 +0000 Subject: [PATCH 22/32] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 3c5ae04a..0f5b59a1 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-7d29d0843a52840291678a3c6d136f496ae1f956853abaa5003c1284ca2c94aa.yml openapi_spec_hash: 010597ad0ec6376fbf2f01ec062787ad -config_hash: ce9765da7e780e630590c72a873f5482 +config_hash: 8f02232be226561c2518368e77bf94cc From 3ae45c2bfc1deebca9b67b064909da5f31d288e8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 21:20:15 +0000 Subject: [PATCH 23/32] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 0f5b59a1..7c3ad8fb 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-7d29d0843a52840291678a3c6d136f496ae1f956853abaa5003c1284ca2c94aa.yml openapi_spec_hash: 010597ad0ec6376fbf2f01ec062787ad -config_hash: 8f02232be226561c2518368e77bf94cc +config_hash: bd8505e17db740d82e578d0edaa9bfe0 From 4865c08579bd2750a17976036da5b513e167112b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 21:30:10 +0000 Subject: [PATCH 24/32] feat(api): api update --- .stats.yml | 4 +- README.md | 17 -------- src/hyperspell/_files.py | 2 +- src/hyperspell/resources/memories.py | 46 +++++++++----------- src/hyperspell/types/memory_upload_params.py | 4 +- tests/api_resources/test_memories.py | 16 +++---- 6 files changed, 33 insertions(+), 56 deletions(-) diff --git a/.stats.yml b/.stats.yml index 7c3ad8fb..f8a5ae17 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-7d29d0843a52840291678a3c6d136f496ae1f956853abaa5003c1284ca2c94aa.yml -openapi_spec_hash: 010597ad0ec6376fbf2f01ec062787ad +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-a73d280435ed6084d8ac9d9d7feb235de6141d866d40725ed26a27b87b0cf364.yml +openapi_spec_hash: 91eabc37804d07ce801b1d4ea1778d1c config_hash: bd8505e17db740d82e578d0edaa9bfe0 diff --git a/README.md b/README.md index 96401a95..cc113942 100644 --- a/README.md +++ b/README.md @@ -200,23 +200,6 @@ query_result = client.memories.search( print(query_result.options) ``` -## File uploads - -Request parameters that correspond to file uploads can be passed as `bytes`, or a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`. - -```python -from pathlib import Path -from hyperspell import Hyperspell - -client = Hyperspell() - -client.memories.upload( - file=Path("/path/to/file"), -) -``` - -The async client uses the exact same interface. If you pass a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance, the file contents will be read asynchronously automatically. - ## Handling errors When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `hyperspell.APIConnectionError` is raised. diff --git a/src/hyperspell/_files.py b/src/hyperspell/_files.py index 155adfec..cc14c14f 100644 --- a/src/hyperspell/_files.py +++ b/src/hyperspell/_files.py @@ -34,7 +34,7 @@ def assert_is_file_content(obj: object, *, key: str | None = None) -> None: if not is_file_content(obj): prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`" raise RuntimeError( - f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead. See https://github.com/hyperspell/python-sdk/tree/main#file-uploads" + f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead." ) from None diff --git a/src/hyperspell/resources/memories.py b/src/hyperspell/resources/memories.py index 9c34885c..65c426da 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, Iterable, Optional, cast +from typing import Dict, List, Union, Iterable, Optional from datetime import datetime from typing_extensions import Literal @@ -16,8 +16,8 @@ memory_upload_params, memory_add_bulk_params, ) -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 .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -549,7 +549,7 @@ def status( def upload( self, *, - file: FileTypes, + file: str, collection: Optional[str] | Omit = omit, metadata: Optional[str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -583,22 +583,20 @@ def upload( timeout: Override the client-level default timeout for this request, in seconds """ - body = deepcopy_minimal( - { - "file": file, - "collection": collection, - "metadata": metadata, - } - ) - files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. # multipart/form-data; boundary=---abc-- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} return self._post( "/memories/upload", - body=maybe_transform(body, memory_upload_params.MemoryUploadParams), - files=files, + body=maybe_transform( + { + "file": file, + "collection": collection, + "metadata": metadata, + }, + memory_upload_params.MemoryUploadParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -1116,7 +1114,7 @@ async def status( async def upload( self, *, - file: FileTypes, + file: str, collection: Optional[str] | Omit = omit, metadata: Optional[str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -1150,22 +1148,20 @@ async def upload( timeout: Override the client-level default timeout for this request, in seconds """ - body = deepcopy_minimal( - { - "file": file, - "collection": collection, - "metadata": metadata, - } - ) - files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. # multipart/form-data; boundary=---abc-- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} return await self._post( "/memories/upload", - body=await async_maybe_transform(body, memory_upload_params.MemoryUploadParams), - files=files, + body=await async_maybe_transform( + { + "file": file, + "collection": collection, + "metadata": metadata, + }, + memory_upload_params.MemoryUploadParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/hyperspell/types/memory_upload_params.py b/src/hyperspell/types/memory_upload_params.py index a906d296..f2968d33 100644 --- a/src/hyperspell/types/memory_upload_params.py +++ b/src/hyperspell/types/memory_upload_params.py @@ -5,13 +5,11 @@ from typing import Optional from typing_extensions import Required, TypedDict -from .._types import FileTypes - __all__ = ["MemoryUploadParams"] class MemoryUploadParams(TypedDict, total=False): - file: Required[FileTypes] + file: Required[str] """The file to ingest.""" collection: Optional[str] diff --git a/tests/api_resources/test_memories.py b/tests/api_resources/test_memories.py index 1b5b4c87..cbef2049 100644 --- a/tests/api_resources/test_memories.py +++ b/tests/api_resources/test_memories.py @@ -388,14 +388,14 @@ def test_streaming_response_status(self, client: Hyperspell) -> None: @parametrize def test_method_upload(self, client: Hyperspell) -> None: memory = client.memories.upload( - file=b"Example data", + file="file", ) assert_matches_type(MemoryStatus, memory, path=["response"]) @parametrize def test_method_upload_with_all_params(self, client: Hyperspell) -> None: memory = client.memories.upload( - file=b"Example data", + file="file", collection="collection", metadata="metadata", ) @@ -404,7 +404,7 @@ def test_method_upload_with_all_params(self, client: Hyperspell) -> None: @parametrize def test_raw_response_upload(self, client: Hyperspell) -> None: response = client.memories.with_raw_response.upload( - file=b"Example data", + file="file", ) assert response.is_closed is True @@ -415,7 +415,7 @@ def test_raw_response_upload(self, client: Hyperspell) -> None: @parametrize def test_streaming_response_upload(self, client: Hyperspell) -> None: with client.memories.with_streaming_response.upload( - file=b"Example data", + file="file", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -793,14 +793,14 @@ async def test_streaming_response_status(self, async_client: AsyncHyperspell) -> @parametrize async def test_method_upload(self, async_client: AsyncHyperspell) -> None: memory = await async_client.memories.upload( - file=b"Example data", + file="file", ) assert_matches_type(MemoryStatus, memory, path=["response"]) @parametrize async def test_method_upload_with_all_params(self, async_client: AsyncHyperspell) -> None: memory = await async_client.memories.upload( - file=b"Example data", + file="file", collection="collection", metadata="metadata", ) @@ -809,7 +809,7 @@ async def test_method_upload_with_all_params(self, async_client: AsyncHyperspell @parametrize async def test_raw_response_upload(self, async_client: AsyncHyperspell) -> None: response = await async_client.memories.with_raw_response.upload( - file=b"Example data", + file="file", ) assert response.is_closed is True @@ -820,7 +820,7 @@ async def test_raw_response_upload(self, async_client: AsyncHyperspell) -> None: @parametrize async def test_streaming_response_upload(self, async_client: AsyncHyperspell) -> None: async with async_client.memories.with_streaming_response.upload( - file=b"Example data", + file="file", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" From 7db075c464314b7077bc555b57ef65e63e464594 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 03:30:30 +0000 Subject: [PATCH 25/32] feat(api): api update --- .stats.yml | 4 +- README.md | 17 ++++++++ src/hyperspell/_files.py | 2 +- src/hyperspell/resources/memories.py | 46 +++++++++++--------- src/hyperspell/types/memory_upload_params.py | 4 +- tests/api_resources/test_memories.py | 16 +++---- 6 files changed, 56 insertions(+), 33 deletions(-) diff --git a/.stats.yml b/.stats.yml index f8a5ae17..7c3ad8fb 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-a73d280435ed6084d8ac9d9d7feb235de6141d866d40725ed26a27b87b0cf364.yml -openapi_spec_hash: 91eabc37804d07ce801b1d4ea1778d1c +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-7d29d0843a52840291678a3c6d136f496ae1f956853abaa5003c1284ca2c94aa.yml +openapi_spec_hash: 010597ad0ec6376fbf2f01ec062787ad config_hash: bd8505e17db740d82e578d0edaa9bfe0 diff --git a/README.md b/README.md index cc113942..96401a95 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,23 @@ query_result = client.memories.search( print(query_result.options) ``` +## File uploads + +Request parameters that correspond to file uploads can be passed as `bytes`, or a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`. + +```python +from pathlib import Path +from hyperspell import Hyperspell + +client = Hyperspell() + +client.memories.upload( + file=Path("/path/to/file"), +) +``` + +The async client uses the exact same interface. If you pass a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance, the file contents will be read asynchronously automatically. + ## Handling errors When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `hyperspell.APIConnectionError` is raised. diff --git a/src/hyperspell/_files.py b/src/hyperspell/_files.py index cc14c14f..155adfec 100644 --- a/src/hyperspell/_files.py +++ b/src/hyperspell/_files.py @@ -34,7 +34,7 @@ def assert_is_file_content(obj: object, *, key: str | None = None) -> None: if not is_file_content(obj): prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`" raise RuntimeError( - f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead." + f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead. See https://github.com/hyperspell/python-sdk/tree/main#file-uploads" ) from None diff --git a/src/hyperspell/resources/memories.py b/src/hyperspell/resources/memories.py index 65c426da..9c34885c 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, Iterable, Optional +from typing import Dict, List, Union, Mapping, Iterable, Optional, cast from datetime import datetime from typing_extensions import Literal @@ -16,8 +16,8 @@ memory_upload_params, memory_add_bulk_params, ) -from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import path_template, maybe_transform, async_maybe_transform +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 .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -549,7 +549,7 @@ def status( def upload( self, *, - file: str, + file: FileTypes, collection: Optional[str] | Omit = omit, metadata: Optional[str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -583,20 +583,22 @@ def upload( timeout: Override the client-level default timeout for this request, in seconds """ + body = deepcopy_minimal( + { + "file": file, + "collection": collection, + "metadata": metadata, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. # multipart/form-data; boundary=---abc-- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} return self._post( "/memories/upload", - body=maybe_transform( - { - "file": file, - "collection": collection, - "metadata": metadata, - }, - memory_upload_params.MemoryUploadParams, - ), + body=maybe_transform(body, memory_upload_params.MemoryUploadParams), + files=files, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -1114,7 +1116,7 @@ async def status( async def upload( self, *, - file: str, + file: FileTypes, collection: Optional[str] | Omit = omit, metadata: Optional[str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -1148,20 +1150,22 @@ async def upload( timeout: Override the client-level default timeout for this request, in seconds """ + body = deepcopy_minimal( + { + "file": file, + "collection": collection, + "metadata": metadata, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. # multipart/form-data; boundary=---abc-- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} return await self._post( "/memories/upload", - body=await async_maybe_transform( - { - "file": file, - "collection": collection, - "metadata": metadata, - }, - memory_upload_params.MemoryUploadParams, - ), + body=await async_maybe_transform(body, memory_upload_params.MemoryUploadParams), + files=files, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/hyperspell/types/memory_upload_params.py b/src/hyperspell/types/memory_upload_params.py index f2968d33..a906d296 100644 --- a/src/hyperspell/types/memory_upload_params.py +++ b/src/hyperspell/types/memory_upload_params.py @@ -5,11 +5,13 @@ from typing import Optional from typing_extensions import Required, TypedDict +from .._types import FileTypes + __all__ = ["MemoryUploadParams"] class MemoryUploadParams(TypedDict, total=False): - file: Required[str] + file: Required[FileTypes] """The file to ingest.""" collection: Optional[str] diff --git a/tests/api_resources/test_memories.py b/tests/api_resources/test_memories.py index cbef2049..1b5b4c87 100644 --- a/tests/api_resources/test_memories.py +++ b/tests/api_resources/test_memories.py @@ -388,14 +388,14 @@ def test_streaming_response_status(self, client: Hyperspell) -> None: @parametrize def test_method_upload(self, client: Hyperspell) -> None: memory = client.memories.upload( - file="file", + file=b"Example data", ) assert_matches_type(MemoryStatus, memory, path=["response"]) @parametrize def test_method_upload_with_all_params(self, client: Hyperspell) -> None: memory = client.memories.upload( - file="file", + file=b"Example data", collection="collection", metadata="metadata", ) @@ -404,7 +404,7 @@ def test_method_upload_with_all_params(self, client: Hyperspell) -> None: @parametrize def test_raw_response_upload(self, client: Hyperspell) -> None: response = client.memories.with_raw_response.upload( - file="file", + file=b"Example data", ) assert response.is_closed is True @@ -415,7 +415,7 @@ def test_raw_response_upload(self, client: Hyperspell) -> None: @parametrize def test_streaming_response_upload(self, client: Hyperspell) -> None: with client.memories.with_streaming_response.upload( - file="file", + file=b"Example data", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -793,14 +793,14 @@ async def test_streaming_response_status(self, async_client: AsyncHyperspell) -> @parametrize async def test_method_upload(self, async_client: AsyncHyperspell) -> None: memory = await async_client.memories.upload( - file="file", + file=b"Example data", ) assert_matches_type(MemoryStatus, memory, path=["response"]) @parametrize async def test_method_upload_with_all_params(self, async_client: AsyncHyperspell) -> None: memory = await async_client.memories.upload( - file="file", + file=b"Example data", collection="collection", metadata="metadata", ) @@ -809,7 +809,7 @@ async def test_method_upload_with_all_params(self, async_client: AsyncHyperspell @parametrize async def test_raw_response_upload(self, async_client: AsyncHyperspell) -> None: response = await async_client.memories.with_raw_response.upload( - file="file", + file=b"Example data", ) assert response.is_closed is True @@ -820,7 +820,7 @@ async def test_raw_response_upload(self, async_client: AsyncHyperspell) -> None: @parametrize async def test_streaming_response_upload(self, async_client: AsyncHyperspell) -> None: async with async_client.memories.with_streaming_response.upload( - file="file", + file=b"Example data", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" From c314c08b0c99131cd98a404ede9416d6c3ff47e3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 05:14:15 +0000 Subject: [PATCH 26/32] fix(client): preserve hardcoded query params when merging with user params --- src/hyperspell/_base_client.py | 4 +++ tests/test_client.py | 48 ++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/hyperspell/_base_client.py b/src/hyperspell/_base_client.py index f01176e8..d1d74b3e 100644 --- a/src/hyperspell/_base_client.py +++ b/src/hyperspell/_base_client.py @@ -540,6 +540,10 @@ def _build_request( files = cast(HttpxRequestFiles, ForceMultipartDict()) prepared_url = self._prepare_url(options.url) + # preserve hard-coded query params from the url + if params and prepared_url.query: + params = {**dict(prepared_url.params.items()), **params} + prepared_url = prepared_url.copy_with(raw_path=prepared_url.raw_path.split(b"?", 1)[0]) if "_" in prepared_url.host: # work around https://github.com/encode/httpx/discussions/2880 kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} diff --git a/tests/test_client.py b/tests/test_client.py index fedd4853..6182649c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -470,6 +470,30 @@ def test_default_query_option(self) -> None: client.close() + def test_hardcoded_query_params_in_url(self, client: Hyperspell) -> None: + request = client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/foo?beta=true", + params={"limit": "10", "page": "abc"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/files/a%2Fb?beta=true", + params={"limit": "10"}, + ) + ) + assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" + def test_request_extra_json(self, client: Hyperspell) -> None: request = client._build_request( FinalRequestOptions( @@ -1434,6 +1458,30 @@ async def test_default_query_option(self) -> None: await client.close() + async def test_hardcoded_query_params_in_url(self, async_client: AsyncHyperspell) -> None: + request = async_client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true"} + + request = async_client._build_request( + FinalRequestOptions( + method="get", + url="/foo?beta=true", + params={"limit": "10", "page": "abc"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} + + request = async_client._build_request( + FinalRequestOptions( + method="get", + url="/files/a%2Fb?beta=true", + params={"limit": "10"}, + ) + ) + assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" + def test_request_extra_json(self, client: Hyperspell) -> None: request = client._build_request( FinalRequestOptions( From ca5e18c4fbab6352479653ca3b70215c7e9e283e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:30:19 +0000 Subject: [PATCH 27/32] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 7c3ad8fb..47b83a91 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-7d29d0843a52840291678a3c6d136f496ae1f956853abaa5003c1284ca2c94aa.yml -openapi_spec_hash: 010597ad0ec6376fbf2f01ec062787ad +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-88c2041bdb5822ea3cbe99c4964f6445dc7c9df525250890d47b761b4a7a2510.yml +openapi_spec_hash: c4ccecb557509b14f5fff33330523964 config_hash: bd8505e17db740d82e578d0edaa9bfe0 From 1cb893b45c0c293a9b613d882a54944e7ef919f1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 9 Apr 2026 02:30:06 +0000 Subject: [PATCH 28/32] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 47b83a91..61b9e21c 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-88c2041bdb5822ea3cbe99c4964f6445dc7c9df525250890d47b761b4a7a2510.yml -openapi_spec_hash: c4ccecb557509b14f5fff33330523964 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-460d7c66cd5e8cf979cd761066c51d8f813a119a20e2149fcfcf847eb650d545.yml +openapi_spec_hash: 8ee512464a88de45c86faf4f46f4905c config_hash: bd8505e17db740d82e578d0edaa9bfe0 From 48cbd64319e2eab5b2915858e47662a84e7e0e76 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 09:02:42 +0000 Subject: [PATCH 29/32] fix: ensure file data are only sent as 1 parameter --- src/hyperspell/_utils/_utils.py | 5 +++-- tests/test_extract_files.py | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/hyperspell/_utils/_utils.py b/src/hyperspell/_utils/_utils.py index eec7f4a1..63b8cd60 100644 --- a/src/hyperspell/_utils/_utils.py +++ b/src/hyperspell/_utils/_utils.py @@ -86,8 +86,9 @@ def _extract_items( index += 1 if is_dict(obj): try: - # We are at the last entry in the path so we must remove the field - if (len(path)) == index: + # Remove the field if there are no more dict keys in the path, + # only "" traversal markers or end. + if all(p == "" for p in path[index:]): item = obj.pop(key) else: item = obj[key] diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py index 00b6250e..da9fb3d5 100644 --- a/tests/test_extract_files.py +++ b/tests/test_extract_files.py @@ -35,6 +35,15 @@ def test_multiple_files() -> None: assert query == {"documents": [{}, {}]} +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 query == {"title": "hello"} + + @pytest.mark.parametrize( "query,paths,expected", [ From 2859f147d2331d1371a1fb61051e89a23e51c077 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 09:06:06 +0000 Subject: [PATCH 30/32] docs: update examples --- README.md | 20 +++++----- tests/api_resources/test_memories.py | 60 ++++++++++++++++------------ tests/test_client.py | 20 +++++----- 3 files changed, 54 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 96401a95..4b0ec922 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ client = Hyperspell( ) memory_status = client.memories.add( - text="text", + text="...", ) print(memory_status.resource_id) ``` @@ -68,7 +68,7 @@ client = AsyncHyperspell( async def main() -> None: memory_status = await client.memories.add( - text="text", + text="...", ) print(memory_status.resource_id) @@ -104,7 +104,7 @@ async def main() -> None: http_client=DefaultAioHttpClient(), ) as client: memory_status = await client.memories.add( - text="text", + text="...", ) print(memory_status.resource_id) @@ -194,8 +194,8 @@ from hyperspell import Hyperspell client = Hyperspell() query_result = client.memories.search( - query="query", - options={}, + query="What does Hyperspell do?", + options={"filter": {}}, ) print(query_result.options) ``` @@ -234,7 +234,7 @@ client = Hyperspell() try: client.memories.add( - text="text", + text="...", ) except hyperspell.APIConnectionError as e: print("The server could not be reached") @@ -279,7 +279,7 @@ client = Hyperspell( # Or, configure per-request: client.with_options(max_retries=5).memories.add( - text="text", + text="...", ) ``` @@ -304,7 +304,7 @@ client = Hyperspell( # Override per-request: client.with_options(timeout=5.0).memories.add( - text="text", + text="...", ) ``` @@ -347,7 +347,7 @@ from hyperspell import Hyperspell client = Hyperspell() response = client.memories.with_raw_response.add( - text="text", + text="...", ) print(response.headers.get('X-My-Header')) @@ -367,7 +367,7 @@ To stream the response body, use `.with_streaming_response` instead, which requi ```python with client.memories.with_streaming_response.add( - text="text", + text="...", ) as response: print(response.headers.get("X-My-Header")) diff --git a/tests/api_resources/test_memories.py b/tests/api_resources/test_memories.py index 1b5b4c87..2a983b3f 100644 --- a/tests/api_resources/test_memories.py +++ b/tests/api_resources/test_memories.py @@ -162,26 +162,30 @@ def test_path_params_delete(self, client: Hyperspell) -> None: @parametrize def test_method_add(self, client: Hyperspell) -> None: memory = client.memories.add( - text="text", + text="...", ) assert_matches_type(MemoryStatus, memory, path=["response"]) @parametrize def test_method_add_with_all_params(self, client: Hyperspell) -> None: memory = client.memories.add( - text="text", - collection="collection", + text="...", + collection="my-collection", date=parse_datetime("2019-12-27T18:11:19.117Z"), - metadata={"foo": "string"}, + metadata={ + "author": "John Doe", + "date": "2025-05-20T02:31:00Z", + "rating": 3, + }, resource_id="resource_id", - title="title", + title="My Document", ) assert_matches_type(MemoryStatus, memory, path=["response"]) @parametrize def test_raw_response_add(self, client: Hyperspell) -> None: response = client.memories.with_raw_response.add( - text="text", + text="...", ) assert response.is_closed is True @@ -192,7 +196,7 @@ def test_raw_response_add(self, client: Hyperspell) -> None: @parametrize def test_streaming_response_add(self, client: Hyperspell) -> None: with client.memories.with_streaming_response.add( - text="text", + text="...", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -278,14 +282,14 @@ def test_path_params_get(self, client: Hyperspell) -> None: @parametrize def test_method_search(self, client: Hyperspell) -> None: memory = client.memories.search( - query="query", + query="What does Hyperspell do?", ) assert_matches_type(QueryResult, memory, path=["response"]) @parametrize def test_method_search_with_all_params(self, client: Hyperspell) -> None: memory = client.memories.search( - query="query", + query="What does Hyperspell do?", answer=True, effort=0, max_results=0, @@ -294,7 +298,7 @@ def test_method_search_with_all_params(self, client: Hyperspell) -> None: "answer_model": "llama-3.1", "before": parse_datetime("2019-12-27T18:11:19.117Z"), "box": {"weight": 0}, - "filter": {"foo": "bar"}, + "filter": {}, "google_calendar": { "calendar_id": "calendar_id", "weight": 0, @@ -332,14 +336,14 @@ def test_method_search_with_all_params(self, client: Hyperspell) -> None: "weight": 0, }, }, - sources=["reddit"], + sources=["vault"], ) assert_matches_type(QueryResult, memory, path=["response"]) @parametrize def test_raw_response_search(self, client: Hyperspell) -> None: response = client.memories.with_raw_response.search( - query="query", + query="What does Hyperspell do?", ) assert response.is_closed is True @@ -350,7 +354,7 @@ def test_raw_response_search(self, client: Hyperspell) -> None: @parametrize def test_streaming_response_search(self, client: Hyperspell) -> None: with client.memories.with_streaming_response.search( - query="query", + query="What does Hyperspell do?", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -567,26 +571,30 @@ async def test_path_params_delete(self, async_client: AsyncHyperspell) -> None: @parametrize async def test_method_add(self, async_client: AsyncHyperspell) -> None: memory = await async_client.memories.add( - text="text", + text="...", ) assert_matches_type(MemoryStatus, memory, path=["response"]) @parametrize async def test_method_add_with_all_params(self, async_client: AsyncHyperspell) -> None: memory = await async_client.memories.add( - text="text", - collection="collection", + text="...", + collection="my-collection", date=parse_datetime("2019-12-27T18:11:19.117Z"), - metadata={"foo": "string"}, + metadata={ + "author": "John Doe", + "date": "2025-05-20T02:31:00Z", + "rating": 3, + }, resource_id="resource_id", - title="title", + title="My Document", ) assert_matches_type(MemoryStatus, memory, path=["response"]) @parametrize async def test_raw_response_add(self, async_client: AsyncHyperspell) -> None: response = await async_client.memories.with_raw_response.add( - text="text", + text="...", ) assert response.is_closed is True @@ -597,7 +605,7 @@ async def test_raw_response_add(self, async_client: AsyncHyperspell) -> None: @parametrize async def test_streaming_response_add(self, async_client: AsyncHyperspell) -> None: async with async_client.memories.with_streaming_response.add( - text="text", + text="...", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -683,14 +691,14 @@ async def test_path_params_get(self, async_client: AsyncHyperspell) -> None: @parametrize async def test_method_search(self, async_client: AsyncHyperspell) -> None: memory = await async_client.memories.search( - query="query", + query="What does Hyperspell do?", ) assert_matches_type(QueryResult, memory, path=["response"]) @parametrize async def test_method_search_with_all_params(self, async_client: AsyncHyperspell) -> None: memory = await async_client.memories.search( - query="query", + query="What does Hyperspell do?", answer=True, effort=0, max_results=0, @@ -699,7 +707,7 @@ async def test_method_search_with_all_params(self, async_client: AsyncHyperspell "answer_model": "llama-3.1", "before": parse_datetime("2019-12-27T18:11:19.117Z"), "box": {"weight": 0}, - "filter": {"foo": "bar"}, + "filter": {}, "google_calendar": { "calendar_id": "calendar_id", "weight": 0, @@ -737,14 +745,14 @@ async def test_method_search_with_all_params(self, async_client: AsyncHyperspell "weight": 0, }, }, - sources=["reddit"], + sources=["vault"], ) assert_matches_type(QueryResult, memory, path=["response"]) @parametrize async def test_raw_response_search(self, async_client: AsyncHyperspell) -> None: response = await async_client.memories.with_raw_response.search( - query="query", + query="What does Hyperspell do?", ) assert response.is_closed is True @@ -755,7 +763,7 @@ async def test_raw_response_search(self, async_client: AsyncHyperspell) -> None: @parametrize async def test_streaming_response_search(self, async_client: AsyncHyperspell) -> None: async with async_client.memories.with_streaming_response.search( - query="query", + query="What does Hyperspell do?", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/test_client.py b/tests/test_client.py index 6182649c..f0f5bcca 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -950,7 +950,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien respx_mock.post("/memories/add").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - client.memories.with_streaming_response.add(text="text").__enter__() + client.memories.with_streaming_response.add(text="...").__enter__() assert _get_open_connections(client) == 0 @@ -960,7 +960,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client respx_mock.post("/memories/add").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - client.memories.with_streaming_response.add(text="text").__enter__() + client.memories.with_streaming_response.add(text="...").__enter__() assert _get_open_connections(client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -989,7 +989,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/memories/add").mock(side_effect=retry_handler) - response = client.memories.with_raw_response.add(text="text") + response = client.memories.with_raw_response.add(text="...") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1013,7 +1013,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/memories/add").mock(side_effect=retry_handler) - response = client.memories.with_raw_response.add(text="text", extra_headers={"x-stainless-retry-count": Omit()}) + response = client.memories.with_raw_response.add(text="...", extra_headers={"x-stainless-retry-count": Omit()}) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1036,7 +1036,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/memories/add").mock(side_effect=retry_handler) - response = client.memories.with_raw_response.add(text="text", extra_headers={"x-stainless-retry-count": "42"}) + response = client.memories.with_raw_response.add(text="...", extra_headers={"x-stainless-retry-count": "42"}) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1951,7 +1951,7 @@ async def test_retrying_timeout_errors_doesnt_leak( respx_mock.post("/memories/add").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await async_client.memories.with_streaming_response.add(text="text").__aenter__() + await async_client.memories.with_streaming_response.add(text="...").__aenter__() assert _get_open_connections(async_client) == 0 @@ -1963,7 +1963,7 @@ async def test_retrying_status_errors_doesnt_leak( respx_mock.post("/memories/add").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await async_client.memories.with_streaming_response.add(text="text").__aenter__() + await async_client.memories.with_streaming_response.add(text="...").__aenter__() assert _get_open_connections(async_client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1992,7 +1992,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/memories/add").mock(side_effect=retry_handler) - response = await client.memories.with_raw_response.add(text="text") + response = await client.memories.with_raw_response.add(text="...") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -2017,7 +2017,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/memories/add").mock(side_effect=retry_handler) response = await client.memories.with_raw_response.add( - text="text", extra_headers={"x-stainless-retry-count": Omit()} + text="...", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -2042,7 +2042,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/memories/add").mock(side_effect=retry_handler) response = await client.memories.with_raw_response.add( - text="text", extra_headers={"x-stainless-retry-count": "42"} + text="...", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From 1d6eca7e4e06048baa53ca84f669dc470c1276ee Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 19:30:30 +0000 Subject: [PATCH 31/32] feat(api): api update --- .stats.yml | 4 ++-- src/hyperspell/resources/actions.py | 8 ++++---- src/hyperspell/resources/memories.py | 20 +++++++++---------- .../types/action_add_reaction_params.py | 2 +- .../types/action_send_message_params.py | 2 +- src/hyperspell/types/auth_me_response.py | 4 ++-- .../types/connection_list_response.py | 2 +- .../types/integration_list_response.py | 2 +- .../web_crawler_index_response.py | 2 +- src/hyperspell/types/memory.py | 2 +- .../types/memory_delete_response.py | 2 +- src/hyperspell/types/memory_list_params.py | 2 +- src/hyperspell/types/memory_search_params.py | 2 +- src/hyperspell/types/memory_status.py | 2 +- src/hyperspell/types/memory_update_params.py | 2 +- src/hyperspell/types/shared/resource.py | 2 +- 16 files changed, 30 insertions(+), 30 deletions(-) diff --git a/.stats.yml b/.stats.yml index 61b9e21c..60ac02f6 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-460d7c66cd5e8cf979cd761066c51d8f813a119a20e2149fcfcf847eb650d545.yml -openapi_spec_hash: 8ee512464a88de45c86faf4f46f4905c +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-6d6dbb68dd9021348431b28e08378d086b3eaf5e65b3dfa03125b1fdec417fa6.yml +openapi_spec_hash: 6ad2b84ac07c482fe838929694e49015 config_hash: bd8505e17db740d82e578d0edaa9bfe0 diff --git a/src/hyperspell/resources/actions.py b/src/hyperspell/resources/actions.py index 40aec472..2537a806 100644 --- a/src/hyperspell/resources/actions.py +++ b/src/hyperspell/resources/actions.py @@ -58,8 +58,8 @@ def add_reaction( "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", @@ -126,8 +126,8 @@ def send_message( "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", @@ -219,8 +219,8 @@ async def add_reaction( "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", @@ -287,8 +287,8 @@ async def send_message( "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", diff --git a/src/hyperspell/resources/memories.py b/src/hyperspell/resources/memories.py index 9c34885c..d731f93a 100644 --- a/src/hyperspell/resources/memories.py +++ b/src/hyperspell/resources/memories.py @@ -71,8 +71,8 @@ def update( "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", @@ -155,8 +155,8 @@ def list( "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", @@ -233,8 +233,8 @@ def delete( "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", @@ -407,8 +407,8 @@ def get( "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", @@ -463,8 +463,8 @@ def search( "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", @@ -638,8 +638,8 @@ async def update( "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", @@ -722,8 +722,8 @@ def list( "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", @@ -800,8 +800,8 @@ async def delete( "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", @@ -974,8 +974,8 @@ async def get( "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", @@ -1030,8 +1030,8 @@ async def search( "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", diff --git a/src/hyperspell/types/action_add_reaction_params.py b/src/hyperspell/types/action_add_reaction_params.py index 98177146..a72da448 100644 --- a/src/hyperspell/types/action_add_reaction_params.py +++ b/src/hyperspell/types/action_add_reaction_params.py @@ -24,8 +24,8 @@ class ActionAddReactionParams(TypedDict, total=False): "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", diff --git a/src/hyperspell/types/action_send_message_params.py b/src/hyperspell/types/action_send_message_params.py index 70d2ddbd..2df01983 100644 --- a/src/hyperspell/types/action_send_message_params.py +++ b/src/hyperspell/types/action_send_message_params.py @@ -18,8 +18,8 @@ class ActionSendMessageParams(TypedDict, total=False): "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", diff --git a/src/hyperspell/types/auth_me_response.py b/src/hyperspell/types/auth_me_response.py index 46e0e2a3..93ea0975 100644 --- a/src/hyperspell/types/auth_me_response.py +++ b/src/hyperspell/types/auth_me_response.py @@ -41,8 +41,8 @@ class AuthMeResponse(BaseModel): "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", @@ -61,8 +61,8 @@ class AuthMeResponse(BaseModel): "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", diff --git a/src/hyperspell/types/connection_list_response.py b/src/hyperspell/types/connection_list_response.py index ac6e71c2..8092529b 100644 --- a/src/hyperspell/types/connection_list_response.py +++ b/src/hyperspell/types/connection_list_response.py @@ -26,8 +26,8 @@ class Connection(BaseModel): "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", diff --git a/src/hyperspell/types/integration_list_response.py b/src/hyperspell/types/integration_list_response.py index 9a62cf8b..47292e38 100644 --- a/src/hyperspell/types/integration_list_response.py +++ b/src/hyperspell/types/integration_list_response.py @@ -32,8 +32,8 @@ class Integration(BaseModel): "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", diff --git a/src/hyperspell/types/integrations/web_crawler_index_response.py b/src/hyperspell/types/integrations/web_crawler_index_response.py index bcbe49cc..16dc13e3 100644 --- a/src/hyperspell/types/integrations/web_crawler_index_response.py +++ b/src/hyperspell/types/integrations/web_crawler_index_response.py @@ -18,8 +18,8 @@ class WebCrawlerIndexResponse(BaseModel): "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", diff --git a/src/hyperspell/types/memory.py b/src/hyperspell/types/memory.py index e1aaa3f9..4ac829ef 100644 --- a/src/hyperspell/types/memory.py +++ b/src/hyperspell/types/memory.py @@ -24,8 +24,8 @@ class Memory(BaseModel): "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", diff --git a/src/hyperspell/types/memory_delete_response.py b/src/hyperspell/types/memory_delete_response.py index 05cb1d63..5f0432d1 100644 --- a/src/hyperspell/types/memory_delete_response.py +++ b/src/hyperspell/types/memory_delete_response.py @@ -22,8 +22,8 @@ class MemoryDeleteResponse(BaseModel): "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", diff --git a/src/hyperspell/types/memory_list_params.py b/src/hyperspell/types/memory_list_params.py index ec2a9044..319f2917 100644 --- a/src/hyperspell/types/memory_list_params.py +++ b/src/hyperspell/types/memory_list_params.py @@ -31,8 +31,8 @@ class MemoryListParams(TypedDict, total=False): "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", diff --git a/src/hyperspell/types/memory_search_params.py b/src/hyperspell/types/memory_search_params.py index f220206f..5719262c 100644 --- a/src/hyperspell/types/memory_search_params.py +++ b/src/hyperspell/types/memory_search_params.py @@ -53,8 +53,8 @@ class MemorySearchParams(TypedDict, total=False): "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", diff --git a/src/hyperspell/types/memory_status.py b/src/hyperspell/types/memory_status.py index fc50b4f5..f30b7c73 100644 --- a/src/hyperspell/types/memory_status.py +++ b/src/hyperspell/types/memory_status.py @@ -18,8 +18,8 @@ class MemoryStatus(BaseModel): "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", diff --git a/src/hyperspell/types/memory_update_params.py b/src/hyperspell/types/memory_update_params.py index 317e419e..d8859d62 100644 --- a/src/hyperspell/types/memory_update_params.py +++ b/src/hyperspell/types/memory_update_params.py @@ -18,8 +18,8 @@ class MemoryUpdateParams(TypedDict, total=False): "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", diff --git a/src/hyperspell/types/shared/resource.py b/src/hyperspell/types/shared/resource.py index 2d5b5472..ccc16b91 100644 --- a/src/hyperspell/types/shared/resource.py +++ b/src/hyperspell/types/shared/resource.py @@ -20,8 +20,8 @@ class Resource(BaseModel): "google_mail", "box", "dropbox", - "google_drive", "github", + "google_drive", "vault", "web_crawler", "trace", From eb6fd0b7e2f7cd9d2c39c83f1f83efd8b2811bdc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 19:30:58 +0000 Subject: [PATCH 32/32] release: 0.37.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 44 +++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- src/hyperspell/_version.py | 2 +- 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 157f0355..51acdaa4 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.36.0" + ".": "0.37.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a32e39f9..86d769ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,49 @@ # Changelog +## 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) + +### Features + +* **api:** api update ([1d6eca7](https://github.com/hyperspell/python-sdk/commit/1d6eca7e4e06048baa53ca84f669dc470c1276ee)) +* **api:** api update ([7db075c](https://github.com/hyperspell/python-sdk/commit/7db075c464314b7077bc555b57ef65e63e464594)) +* **api:** api update ([4865c08](https://github.com/hyperspell/python-sdk/commit/4865c08579bd2750a17976036da5b513e167112b)) +* **api:** api update ([13e0682](https://github.com/hyperspell/python-sdk/commit/13e068243400026b7c8e11b11fdca6ba89a2460e)) +* **api:** api update ([a0f4720](https://github.com/hyperspell/python-sdk/commit/a0f47209c51eb35cf31974bc158cf9e4df29c0ec)) +* **api:** api update ([d3e77c3](https://github.com/hyperspell/python-sdk/commit/d3e77c368350f9a22469db6574d3a44159a043c9)) +* **api:** api update ([6180902](https://github.com/hyperspell/python-sdk/commit/618090275519b615d2f0b4679aecb9a86ccbab0b)) +* **internal:** implement indices array format for query and form serialization ([0e1fd91](https://github.com/hyperspell/python-sdk/commit/0e1fd91815afe92b4b7ba73f96e196aa71d5f8c5)) + + +### Bug Fixes + +* **client:** preserve hardcoded query params when merging with user params ([c314c08](https://github.com/hyperspell/python-sdk/commit/c314c08b0c99131cd98a404ede9416d6c3ff47e3)) +* ensure file data are only sent as 1 parameter ([48cbd64](https://github.com/hyperspell/python-sdk/commit/48cbd64319e2eab5b2915858e47662a84e7e0e76)) +* sanitize endpoint path params ([b2b3ca2](https://github.com/hyperspell/python-sdk/commit/b2b3ca2749930db6558bbe5bc3bb1ce4adadfe0e)) + + +### Chores + +* **ci:** skip lint on metadata-only changes ([b5a6b2d](https://github.com/hyperspell/python-sdk/commit/b5a6b2d8467b72cc0bfa192cf2bfd2966de6190e)) +* **internal:** update gitignore ([8f27c46](https://github.com/hyperspell/python-sdk/commit/8f27c46407285a62b885632d2bb3c6059ec91eb2)) +* **tests:** bump steady to v0.19.4 ([a855b0a](https://github.com/hyperspell/python-sdk/commit/a855b0a42e5926bdb4c1ece32f74370f9b481241)) +* **tests:** bump steady to v0.19.5 ([7ea97c0](https://github.com/hyperspell/python-sdk/commit/7ea97c0c00ee4d81301a6b2214ed677e88a5fcd8)) +* **tests:** bump steady to v0.19.6 ([1ecd0bf](https://github.com/hyperspell/python-sdk/commit/1ecd0bfd11ca3de3436524145c5df5c20b2ec2eb)) +* **tests:** bump steady to v0.19.7 ([466a814](https://github.com/hyperspell/python-sdk/commit/466a8145d00c3810bd1d83ac8531c719377100b8)) +* **tests:** bump steady to v0.20.1 ([74c3b3b](https://github.com/hyperspell/python-sdk/commit/74c3b3b7867b0fef233c314c776c5d867f66937a)) +* **tests:** bump steady to v0.20.2 ([3eb71a2](https://github.com/hyperspell/python-sdk/commit/3eb71a2ea44943f9b2e10073292d7fd4d58c1b43)) + + +### Documentation + +* update examples ([2859f14](https://github.com/hyperspell/python-sdk/commit/2859f147d2331d1371a1fb61051e89a23e51c077)) + + +### Refactors + +* **tests:** switch from prism to steady ([441e854](https://github.com/hyperspell/python-sdk/commit/441e854a4933f7abd07961f08a58d7f7b7b3ea60)) + ## 0.36.0 (2026-03-18) Full Changelog: [v0.35.0...v0.36.0](https://github.com/hyperspell/python-sdk/compare/v0.35.0...v0.36.0) diff --git a/pyproject.toml b/pyproject.toml index 94f3c49e..7a6fa077 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "hyperspell" -version = "0.36.0" +version = "0.37.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 77a70486..ea4e056d 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.36.0" # x-release-please-version +__version__ = "0.37.0" # x-release-please-version