diff --git a/.gitignore b/.gitignore
index 87797408..95ceb189 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,4 @@
.prism.log
-.vscode
_dev
__pycache__
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index e7562934..0c2ecec6 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.19.0"
+ ".": "0.20.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index 2c00fa68..70f6684a 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 12
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-4bfa5ab6a0021f526d6f6f622f1872f2ec3467b6d1bd51acfc17bfc6f399f915.yml
-openapi_spec_hash: 5b51df5010bab70a1f3f884552ce25c3
-config_hash: c894437241b21cedd2d01854f1c7a8ef
+configured_endpoints: 13
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-e8dc461bfd68745a68eddc289ac15c0c5d222f20e81a5c61b11ee3807a75d839.yml
+openapi_spec_hash: 24eece3c79152b96c2cc0f398cf9d6af
+config_hash: bf6196b98ec72829d458bc45ccd5a5f9
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..5b010307
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "python.analysis.importFormat": "relative",
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e3ee8045..fe1e190f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,28 @@
# Changelog
+## 0.20.0 (2025-08-01)
+
+Full Changelog: [v0.19.0...v0.20.0](https://github.com/hyperspell/python-sdk/compare/v0.19.0...v0.20.0)
+
+### Features
+
+* **api:** api update ([6b411be](https://github.com/hyperspell/python-sdk/commit/6b411be3d85dbc3106741cfbfa08454ce443b173))
+* **api:** api update ([0789efa](https://github.com/hyperspell/python-sdk/commit/0789efa8c1e2762ce8bf5329a7411dba886709dd))
+* **api:** update via SDK Studio ([fd10b3b](https://github.com/hyperspell/python-sdk/commit/fd10b3b4b26774c5fcd033481712fda2cb77cc80))
+* **api:** update via SDK Studio ([a92de5a](https://github.com/hyperspell/python-sdk/commit/a92de5a8dbc7c6b516b58d159840657c356cf423))
+* **client:** support file upload requests ([98a9c26](https://github.com/hyperspell/python-sdk/commit/98a9c26c10636926fb05844b3494707e5d64babf))
+
+
+### Bug Fixes
+
+* **parsing:** ignore empty metadata ([01e34f8](https://github.com/hyperspell/python-sdk/commit/01e34f8e8697e5380884a0d93dd6b2d248f578d0))
+* **parsing:** parse extra field types ([167331c](https://github.com/hyperspell/python-sdk/commit/167331c90f0001fa4165d2cafa185c6017383dad))
+
+
+### Chores
+
+* **project:** add settings file for vscode ([f597f95](https://github.com/hyperspell/python-sdk/commit/f597f9542a2572674dfc0a97fd94950cee114edf))
+
## 0.19.0 (2025-07-18)
Full Changelog: [v0.18.0...v0.19.0](https://github.com/hyperspell/python-sdk/compare/v0.18.0...v0.19.0)
diff --git a/LICENSE b/LICENSE
index 85e0f735..9239282a 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,201 +1,7 @@
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
+Copyright 2025 hyperspell
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
- 1. Definitions.
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright 2025 Hyperspell
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
index 5890113e..9a5f7b4a 100644
--- a/README.md
+++ b/README.md
@@ -193,9 +193,9 @@ client = Hyperspell()
response = client.memories.search(
query="query",
- filter={},
+ options={},
)
-print(response.filter)
+print(response.options)
```
## File uploads
diff --git a/api.md b/api.md
index bba49d1a..f59feeb3 100644
--- a/api.md
+++ b/api.md
@@ -39,12 +39,19 @@ Methods:
Types:
```python
-from hyperspell.types import Memory, MemoryStatus, MemorySearchResponse, MemoryStatusResponse
+from hyperspell.types import (
+ Memory,
+ MemoryStatus,
+ MemoryDeleteResponse,
+ MemorySearchResponse,
+ MemoryStatusResponse,
+)
```
Methods:
- client.memories.list(\*\*params) -> SyncCursorPage[Memory]
+- client.memories.delete(resource_id, \*, source) -> MemoryDeleteResponse
- client.memories.add(\*\*params) -> MemoryStatus
- client.memories.get(resource_id, \*, source) -> Memory
- client.memories.search(\*\*params) -> MemorySearchResponse
diff --git a/pyproject.toml b/pyproject.toml
index b08762c7..3a9b3f08 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,9 +1,9 @@
[project]
name = "hyperspell"
-version = "0.19.0"
+version = "0.20.0"
description = "The official Python library for the hyperspell API"
dynamic = ["readme"]
-license = "Apache-2.0"
+license = "MIT"
authors = [
{ name = "Hyperspell", email = "hello@hyperspell.com" },
]
@@ -31,7 +31,7 @@ classifiers = [
"Operating System :: POSIX :: Linux",
"Operating System :: Microsoft :: Windows",
"Topic :: Software Development :: Libraries :: Python Modules",
- "License :: OSI Approved :: Apache Software License"
+ "License :: OSI Approved :: MIT License"
]
[project.urls]
diff --git a/src/hyperspell/_base_client.py b/src/hyperspell/_base_client.py
index 93ac9ef9..9ff21427 100644
--- a/src/hyperspell/_base_client.py
+++ b/src/hyperspell/_base_client.py
@@ -532,7 +532,10 @@ def _build_request(
is_body_allowed = options.method.lower() != "get"
if is_body_allowed:
- kwargs["json"] = json_data if is_given(json_data) else None
+ if isinstance(json_data, bytes):
+ kwargs["content"] = json_data
+ else:
+ kwargs["json"] = json_data if is_given(json_data) else None
kwargs["files"] = files
else:
headers.pop("Content-Type", None)
diff --git a/src/hyperspell/_files.py b/src/hyperspell/_files.py
index 1d0a3358..155adfec 100644
--- a/src/hyperspell/_files.py
+++ b/src/hyperspell/_files.py
@@ -69,12 +69,12 @@ def _transform_file(file: FileTypes) -> HttpxFileTypes:
return file
if is_tuple_t(file):
- return (file[0], _read_file_content(file[1]), *file[2:])
+ return (file[0], read_file_content(file[1]), *file[2:])
raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple")
-def _read_file_content(file: FileContent) -> HttpxFileContent:
+def read_file_content(file: FileContent) -> HttpxFileContent:
if isinstance(file, os.PathLike):
return pathlib.Path(file).read_bytes()
return file
@@ -111,12 +111,12 @@ async def _async_transform_file(file: FileTypes) -> HttpxFileTypes:
return file
if is_tuple_t(file):
- return (file[0], await _async_read_file_content(file[1]), *file[2:])
+ return (file[0], await async_read_file_content(file[1]), *file[2:])
raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple")
-async def _async_read_file_content(file: FileContent) -> HttpxFileContent:
+async def async_read_file_content(file: FileContent) -> HttpxFileContent:
if isinstance(file, os.PathLike):
return await anyio.Path(file).read_bytes()
diff --git a/src/hyperspell/_models.py b/src/hyperspell/_models.py
index 528d5680..b8387ce9 100644
--- a/src/hyperspell/_models.py
+++ b/src/hyperspell/_models.py
@@ -208,14 +208,18 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride]
else:
fields_values[name] = field_get_default(field)
+ extra_field_type = _get_extra_fields_type(__cls)
+
_extra = {}
for key, value in values.items():
if key not in model_fields:
+ parsed = construct_type(value=value, type_=extra_field_type) if extra_field_type is not None else value
+
if PYDANTIC_V2:
- _extra[key] = value
+ _extra[key] = parsed
else:
_fields_set.add(key)
- fields_values[key] = value
+ fields_values[key] = parsed
object.__setattr__(m, "__dict__", fields_values)
@@ -370,6 +374,23 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object:
return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None))
+def _get_extra_fields_type(cls: type[pydantic.BaseModel]) -> type | None:
+ if not PYDANTIC_V2:
+ # TODO
+ return None
+
+ schema = cls.__pydantic_core_schema__
+ if schema["type"] == "model":
+ fields = schema["schema"]
+ if fields["type"] == "model-fields":
+ extras = fields.get("extras_schema")
+ if extras and "cls" in extras:
+ # mypy can't narrow the type
+ return extras["cls"] # type: ignore[no-any-return]
+
+ return None
+
+
def is_basemodel(type_: type) -> bool:
"""Returns whether or not the given type is either a `BaseModel` or a union of `BaseModel`"""
if is_union(type_):
@@ -439,7 +460,7 @@ def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]
type_ = type_.__value__ # type: ignore[unreachable]
# unwrap `Annotated[T, ...]` -> `T`
- if metadata is not None:
+ if metadata is not None and len(metadata) > 0:
meta: tuple[Any, ...] = tuple(metadata)
elif is_annotated_type(type_):
meta = get_args(type_)[1:]
diff --git a/src/hyperspell/_version.py b/src/hyperspell/_version.py
index 60454a0d..b381fc89 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.19.0" # x-release-please-version
+__version__ = "0.20.0" # x-release-please-version
diff --git a/src/hyperspell/resources/memories.py b/src/hyperspell/resources/memories.py
index 94ed512e..14082a27 100644
--- a/src/hyperspell/resources/memories.py
+++ b/src/hyperspell/resources/memories.py
@@ -23,6 +23,7 @@
from .._base_client import AsyncPaginator, make_request_options
from ..types.memory import Memory
from ..types.memory_status import MemoryStatus
+from ..types.memory_delete_response import MemoryDeleteResponse
from ..types.memory_search_response import MemorySearchResponse
from ..types.memory_status_response import MemoryStatusResponse
@@ -152,6 +153,103 @@ def list(
model=Memory,
)
+ def delete(
+ self,
+ resource_id: str,
+ *,
+ source: Literal[
+ "collections",
+ "vault",
+ "web_crawler",
+ "notion",
+ "slack",
+ "google_calendar",
+ "reddit",
+ "box",
+ "google_drive",
+ "airtable",
+ "algolia",
+ "amplitude",
+ "asana",
+ "ashby",
+ "bamboohr",
+ "basecamp",
+ "bubbles",
+ "calendly",
+ "confluence",
+ "clickup",
+ "datadog",
+ "deel",
+ "discord",
+ "dropbox",
+ "exa",
+ "facebook",
+ "front",
+ "github",
+ "gitlab",
+ "google_docs",
+ "google_mail",
+ "google_sheet",
+ "hubspot",
+ "jira",
+ "linear",
+ "microsoft_teams",
+ "mixpanel",
+ "monday",
+ "outlook",
+ "perplexity",
+ "rippling",
+ "salesforce",
+ "segment",
+ "todoist",
+ "twitter",
+ "zoom",
+ ],
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ ) -> MemoryDeleteResponse:
+ """
+ Delete a memory and its associated chunks from the index.
+
+ This removes the memory completely from the vector index and database. The
+ operation deletes:
+
+ 1. All chunks associated with the resource (including embeddings)
+ 2. The resource record itself
+
+ Args: source: The document provider (e.g., gmail, notion, vault) resource_id:
+ The unique identifier of the resource to delete api_token: Authentication token
+
+ Returns: MemoryDeletionResponse with deletion details
+
+ Raises: DocumentNotFound: If the resource doesn't exist or user doesn't have
+ access
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not source:
+ raise ValueError(f"Expected a non-empty value for `source` but received {source!r}")
+ 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}",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=MemoryDeleteResponse,
+ )
+
def add(
self,
*,
@@ -302,7 +400,6 @@ def search(
*,
query: str,
answer: bool | NotGiven = NOT_GIVEN,
- filter: Optional[memory_search_params.Filter] | NotGiven = NOT_GIVEN,
max_results: int | NotGiven = NOT_GIVEN,
options: memory_search_params.Options | NotGiven = NOT_GIVEN,
sources: List[
@@ -371,8 +468,6 @@ def search(
answer: If true, the query will be answered along with matching source documents.
- filter: DEPRECATED: Use options instead. This field will be removed in a future version.
-
max_results: Maximum number of results to return.
options: Search options for the query.
@@ -393,7 +488,6 @@ def search(
{
"query": query,
"answer": answer,
- "filter": filter,
"max_results": max_results,
"options": options,
"sources": sources,
@@ -605,6 +699,103 @@ def list(
model=Memory,
)
+ async def delete(
+ self,
+ resource_id: str,
+ *,
+ source: Literal[
+ "collections",
+ "vault",
+ "web_crawler",
+ "notion",
+ "slack",
+ "google_calendar",
+ "reddit",
+ "box",
+ "google_drive",
+ "airtable",
+ "algolia",
+ "amplitude",
+ "asana",
+ "ashby",
+ "bamboohr",
+ "basecamp",
+ "bubbles",
+ "calendly",
+ "confluence",
+ "clickup",
+ "datadog",
+ "deel",
+ "discord",
+ "dropbox",
+ "exa",
+ "facebook",
+ "front",
+ "github",
+ "gitlab",
+ "google_docs",
+ "google_mail",
+ "google_sheet",
+ "hubspot",
+ "jira",
+ "linear",
+ "microsoft_teams",
+ "mixpanel",
+ "monday",
+ "outlook",
+ "perplexity",
+ "rippling",
+ "salesforce",
+ "segment",
+ "todoist",
+ "twitter",
+ "zoom",
+ ],
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
+ ) -> MemoryDeleteResponse:
+ """
+ Delete a memory and its associated chunks from the index.
+
+ This removes the memory completely from the vector index and database. The
+ operation deletes:
+
+ 1. All chunks associated with the resource (including embeddings)
+ 2. The resource record itself
+
+ Args: source: The document provider (e.g., gmail, notion, vault) resource_id:
+ The unique identifier of the resource to delete api_token: Authentication token
+
+ Returns: MemoryDeletionResponse with deletion details
+
+ Raises: DocumentNotFound: If the resource doesn't exist or user doesn't have
+ access
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ if not source:
+ raise ValueError(f"Expected a non-empty value for `source` but received {source!r}")
+ 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}",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=MemoryDeleteResponse,
+ )
+
async def add(
self,
*,
@@ -755,7 +946,6 @@ async def search(
*,
query: str,
answer: bool | NotGiven = NOT_GIVEN,
- filter: Optional[memory_search_params.Filter] | NotGiven = NOT_GIVEN,
max_results: int | NotGiven = NOT_GIVEN,
options: memory_search_params.Options | NotGiven = NOT_GIVEN,
sources: List[
@@ -824,8 +1014,6 @@ async def search(
answer: If true, the query will be answered along with matching source documents.
- filter: DEPRECATED: Use options instead. This field will be removed in a future version.
-
max_results: Maximum number of results to return.
options: Search options for the query.
@@ -846,7 +1034,6 @@ async def search(
{
"query": query,
"answer": answer,
- "filter": filter,
"max_results": max_results,
"options": options,
"sources": sources,
@@ -942,6 +1129,9 @@ def __init__(self, memories: MemoriesResource) -> None:
self.list = to_raw_response_wrapper(
memories.list,
)
+ self.delete = to_raw_response_wrapper(
+ memories.delete,
+ )
self.add = to_raw_response_wrapper(
memories.add,
)
@@ -966,6 +1156,9 @@ def __init__(self, memories: AsyncMemoriesResource) -> None:
self.list = async_to_raw_response_wrapper(
memories.list,
)
+ self.delete = async_to_raw_response_wrapper(
+ memories.delete,
+ )
self.add = async_to_raw_response_wrapper(
memories.add,
)
@@ -990,6 +1183,9 @@ def __init__(self, memories: MemoriesResource) -> None:
self.list = to_streamed_response_wrapper(
memories.list,
)
+ self.delete = to_streamed_response_wrapper(
+ memories.delete,
+ )
self.add = to_streamed_response_wrapper(
memories.add,
)
@@ -1014,6 +1210,9 @@ def __init__(self, memories: AsyncMemoriesResource) -> None:
self.list = async_to_streamed_response_wrapper(
memories.list,
)
+ self.delete = async_to_streamed_response_wrapper(
+ memories.delete,
+ )
self.add = async_to_streamed_response_wrapper(
memories.add,
)
diff --git a/src/hyperspell/types/__init__.py b/src/hyperspell/types/__init__.py
index bfaf01aa..1cb2bdab 100644
--- a/src/hyperspell/types/__init__.py
+++ b/src/hyperspell/types/__init__.py
@@ -13,6 +13,7 @@
from .memory_search_params import MemorySearchParams as MemorySearchParams
from .memory_upload_params import MemoryUploadParams as MemoryUploadParams
from .auth_user_token_params import AuthUserTokenParams as AuthUserTokenParams
+from .memory_delete_response import MemoryDeleteResponse as MemoryDeleteResponse
from .memory_search_response import MemorySearchResponse as MemorySearchResponse
from .memory_status_response import MemoryStatusResponse as MemoryStatusResponse
from .integration_revoke_response import IntegrationRevokeResponse as IntegrationRevokeResponse
diff --git a/src/hyperspell/types/memory.py b/src/hyperspell/types/memory.py
index 0b9b3fe7..021ba64d 100644
--- a/src/hyperspell/types/memory.py
+++ b/src/hyperspell/types/memory.py
@@ -1,9 +1,11 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import TYPE_CHECKING, List, Optional
+from typing import TYPE_CHECKING, Dict, List, Optional
from datetime import datetime
from typing_extensions import Literal
+from pydantic import Field as FieldInfo
+
from .._models import BaseModel
__all__ = ["Memory", "Metadata", "MetadataEvent"]
@@ -18,6 +20,8 @@ class MetadataEvent(BaseModel):
class Metadata(BaseModel):
+ created_at: Optional[datetime] = None
+
events: Optional[List[MetadataEvent]] = None
indexed_at: Optional[datetime] = None
@@ -26,6 +30,9 @@ class Metadata(BaseModel):
status: Optional[Literal["pending", "processing", "completed", "failed"]] = None
+ url: Optional[str] = None
+
+ __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride]
if TYPE_CHECKING:
# Stub to indicate that arbitrary properties are accepted.
# To access properties that are not valid identifiers you can use `getattr`, e.g.
diff --git a/src/hyperspell/types/memory_delete_response.py b/src/hyperspell/types/memory_delete_response.py
new file mode 100644
index 00000000..7f5fbd62
--- /dev/null
+++ b/src/hyperspell/types/memory_delete_response.py
@@ -0,0 +1,66 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing_extensions import Literal
+
+from .._models import BaseModel
+
+__all__ = ["MemoryDeleteResponse"]
+
+
+class MemoryDeleteResponse(BaseModel):
+ chunks_deleted: int
+
+ message: str
+
+ resource_id: str
+
+ source: Literal[
+ "collections",
+ "vault",
+ "web_crawler",
+ "notion",
+ "slack",
+ "google_calendar",
+ "reddit",
+ "box",
+ "google_drive",
+ "airtable",
+ "algolia",
+ "amplitude",
+ "asana",
+ "ashby",
+ "bamboohr",
+ "basecamp",
+ "bubbles",
+ "calendly",
+ "confluence",
+ "clickup",
+ "datadog",
+ "deel",
+ "discord",
+ "dropbox",
+ "exa",
+ "facebook",
+ "front",
+ "github",
+ "gitlab",
+ "google_docs",
+ "google_mail",
+ "google_sheet",
+ "hubspot",
+ "jira",
+ "linear",
+ "microsoft_teams",
+ "mixpanel",
+ "monday",
+ "outlook",
+ "perplexity",
+ "rippling",
+ "salesforce",
+ "segment",
+ "todoist",
+ "twitter",
+ "zoom",
+ ]
+
+ success: bool
diff --git a/src/hyperspell/types/memory_search_params.py b/src/hyperspell/types/memory_search_params.py
index 9f97bc11..8c3940cb 100644
--- a/src/hyperspell/types/memory_search_params.py
+++ b/src/hyperspell/types/memory_search_params.py
@@ -10,15 +10,11 @@
__all__ = [
"MemorySearchParams",
- "Filter",
- "FilterGoogleCalendar",
- "FilterGoogleMail",
- "FilterNotion",
- "FilterReddit",
- "FilterSlack",
- "FilterWebCrawler",
"Options",
+ "OptionsBox",
+ "OptionsCollections",
"OptionsGoogleCalendar",
+ "OptionsGoogleDrive",
"OptionsGoogleMail",
"OptionsNotion",
"OptionsReddit",
@@ -34,12 +30,6 @@ class MemorySearchParams(TypedDict, total=False):
answer: bool
"""If true, the query will be answered along with matching source documents."""
- filter: Optional[Filter]
- """DEPRECATED: Use options instead.
-
- This field will be removed in a future version.
- """
-
max_results: int
"""Maximum number of results to return."""
@@ -99,111 +89,84 @@ class MemorySearchParams(TypedDict, total=False):
"""Only query documents from these sources."""
-class FilterGoogleCalendar(TypedDict, total=False):
- calendar_id: Optional[str]
- """The ID of the calendar to search.
-
- If not provided, it will use the ID of the default calendar. You can get the
- list of calendars with the `/integrations/google_calendar/list` endpoint.
- """
-
-
-class FilterGoogleMail(TypedDict, total=False):
- label_ids: List[str]
- """List of label IDs to filter messages (e.g., ['INBOX', 'SENT', 'DRAFT']).
-
- Multiple labels are combined with OR logic - messages matching ANY specified
- label will be returned. If empty, no label filtering is applied (searches all
- accessible messages).
- """
+class OptionsBox(TypedDict, total=False):
+ after: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
+ """Only query documents created on or after this date."""
+ before: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
+ """Only query documents created before this date."""
-class FilterNotion(TypedDict, total=False):
- notion_page_ids: List[str]
- """List of Notion page IDs to search.
+ weight: float
+ """Weight of results from this source.
- If not provided, all pages in the workspace will be searched.
+ A weight greater than 1.0 means more results from this source will be returned,
+ a weight less than 1.0 means fewer results will be returned. This will only
+ affect results if multiple sources are queried at the same time.
"""
-class FilterReddit(TypedDict, total=False):
- period: Literal["hour", "day", "week", "month", "year", "all"]
- """The time period to search. Defaults to 'month'."""
-
- sort: Literal["relevance", "new", "hot", "top", "comments"]
- """The sort order of the posts. Defaults to 'relevance'."""
-
- subreddit: Optional[str]
- """The subreddit to search.
-
- If not provided, the query will be searched for in all subreddits.
- """
+class OptionsCollections(TypedDict, total=False):
+ after: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
+ """Only query documents created on or after this date."""
+ before: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
+ """Only query documents created before this date."""
-class FilterSlack(TypedDict, total=False):
- channels: List[str]
- """List of Slack channels to search.
+ weight: float
+ """Weight of results from this source.
- If not provided, all channels in the workspace will be searched.
+ A weight greater than 1.0 means more results from this source will be returned,
+ a weight less than 1.0 means fewer results will be returned. This will only
+ affect results if multiple sources are queried at the same time.
"""
-class FilterWebCrawler(TypedDict, total=False):
- max_depth: int
- """Maximum depth to crawl from the starting URL"""
-
- url: Union[str, object]
- """The URL to crawl"""
-
-
-class Filter(TypedDict, total=False):
+class OptionsGoogleCalendar(TypedDict, total=False):
after: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
"""Only query documents created on or after this date."""
- answer_model: Literal["llama-3.1", "gemma2", "qwen-qwq", "mistral-saba", "llama-4-scout", "deepseek-r1"]
- """Model to use for answer generation when answer=True"""
-
before: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
"""Only query documents created before this date."""
- box: object
- """Search options for Box"""
-
- collections: object
- """Search options for vault"""
-
- google_calendar: FilterGoogleCalendar
- """Search options for Google Calendar"""
-
- google_drive: object
- """Search options for Google Drive"""
+ calendar_id: Optional[str]
+ """The ID of the calendar to search.
- google_mail: FilterGoogleMail
- """Search options for Gmail"""
+ If not provided, it will use the ID of the default calendar. You can get the
+ list of calendars with the `/integrations/google_calendar/list` endpoint.
+ """
- notion: FilterNotion
- """Search options for Notion"""
+ weight: float
+ """Weight of results from this source.
- reddit: FilterReddit
- """Search options for Reddit"""
+ A weight greater than 1.0 means more results from this source will be returned,
+ a weight less than 1.0 means fewer results will be returned. This will only
+ affect results if multiple sources are queried at the same time.
+ """
- slack: FilterSlack
- """Search options for Slack"""
- web_crawler: FilterWebCrawler
- """Search options for Web Crawler"""
+class OptionsGoogleDrive(TypedDict, total=False):
+ after: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
+ """Only query documents created on or after this date."""
+ before: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
+ """Only query documents created before this date."""
-class OptionsGoogleCalendar(TypedDict, total=False):
- calendar_id: Optional[str]
- """The ID of the calendar to search.
+ weight: float
+ """Weight of results from this source.
- If not provided, it will use the ID of the default calendar. You can get the
- list of calendars with the `/integrations/google_calendar/list` endpoint.
+ A weight greater than 1.0 means more results from this source will be returned,
+ a weight less than 1.0 means fewer results will be returned. This will only
+ affect results if multiple sources are queried at the same time.
"""
class OptionsGoogleMail(TypedDict, total=False):
+ after: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
+ """Only query documents created on or after this date."""
+
+ before: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
+ """Only query documents created before this date."""
+
label_ids: List[str]
"""List of label IDs to filter messages (e.g., ['INBOX', 'SENT', 'DRAFT']).
@@ -212,16 +175,44 @@ class OptionsGoogleMail(TypedDict, total=False):
accessible messages).
"""
+ weight: float
+ """Weight of results from this source.
+
+ A weight greater than 1.0 means more results from this source will be returned,
+ a weight less than 1.0 means fewer results will be returned. This will only
+ affect results if multiple sources are queried at the same time.
+ """
+
class OptionsNotion(TypedDict, total=False):
+ after: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
+ """Only query documents created on or after this date."""
+
+ before: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
+ """Only query documents created before this date."""
+
notion_page_ids: List[str]
"""List of Notion page IDs to search.
If not provided, all pages in the workspace will be searched.
"""
+ weight: float
+ """Weight of results from this source.
+
+ A weight greater than 1.0 means more results from this source will be returned,
+ a weight less than 1.0 means fewer results will be returned. This will only
+ affect results if multiple sources are queried at the same time.
+ """
+
class OptionsReddit(TypedDict, total=False):
+ after: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
+ """Only query documents created on or after this date."""
+
+ before: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
+ """Only query documents created before this date."""
+
period: Literal["hour", "day", "week", "month", "year", "all"]
"""The time period to search. Defaults to 'month'."""
@@ -234,22 +225,58 @@ class OptionsReddit(TypedDict, total=False):
If not provided, the query will be searched for in all subreddits.
"""
+ weight: float
+ """Weight of results from this source.
+
+ A weight greater than 1.0 means more results from this source will be returned,
+ a weight less than 1.0 means fewer results will be returned. This will only
+ affect results if multiple sources are queried at the same time.
+ """
+
class OptionsSlack(TypedDict, total=False):
+ after: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
+ """Only query documents created on or after this date."""
+
+ before: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
+ """Only query documents created before this date."""
+
channels: List[str]
"""List of Slack channels to search.
If not provided, all channels in the workspace will be searched.
"""
+ weight: float
+ """Weight of results from this source.
+
+ A weight greater than 1.0 means more results from this source will be returned,
+ a weight less than 1.0 means fewer results will be returned. This will only
+ affect results if multiple sources are queried at the same time.
+ """
+
class OptionsWebCrawler(TypedDict, total=False):
+ after: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
+ """Only query documents created on or after this date."""
+
+ before: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
+ """Only query documents created before this date."""
+
max_depth: int
"""Maximum depth to crawl from the starting URL"""
url: Union[str, object]
"""The URL to crawl"""
+ weight: float
+ """Weight of results from this source.
+
+ A weight greater than 1.0 means more results from this source will be returned,
+ a weight less than 1.0 means fewer results will be returned. This will only
+ affect results if multiple sources are queried at the same time.
+ """
+
class Options(TypedDict, total=False):
after: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
@@ -261,21 +288,24 @@ class Options(TypedDict, total=False):
before: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")]
"""Only query documents created before this date."""
- box: object
+ box: OptionsBox
"""Search options for Box"""
- collections: object
+ collections: OptionsCollections
"""Search options for vault"""
google_calendar: OptionsGoogleCalendar
"""Search options for Google Calendar"""
- google_drive: object
+ google_drive: OptionsGoogleDrive
"""Search options for Google Drive"""
google_mail: OptionsGoogleMail
"""Search options for Gmail"""
+ max_results: int
+ """Maximum number of results to return."""
+
notion: OptionsNotion
"""Search options for Notion"""
diff --git a/tests/api_resources/test_memories.py b/tests/api_resources/test_memories.py
index b08ba4a7..cd3041d8 100644
--- a/tests/api_resources/test_memories.py
+++ b/tests/api_resources/test_memories.py
@@ -12,6 +12,7 @@
from hyperspell.types import (
Memory,
MemoryStatus,
+ MemoryDeleteResponse,
MemorySearchResponse,
MemoryStatusResponse,
)
@@ -59,6 +60,48 @@ def test_streaming_response_list(self, client: Hyperspell) -> None:
assert cast(Any, response.is_closed) is True
+ @parametrize
+ def test_method_delete(self, client: Hyperspell) -> None:
+ memory = client.memories.delete(
+ resource_id="resource_id",
+ source="collections",
+ )
+ assert_matches_type(MemoryDeleteResponse, memory, path=["response"])
+
+ @parametrize
+ def test_raw_response_delete(self, client: Hyperspell) -> None:
+ response = client.memories.with_raw_response.delete(
+ resource_id="resource_id",
+ source="collections",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ memory = response.parse()
+ assert_matches_type(MemoryDeleteResponse, memory, path=["response"])
+
+ @parametrize
+ def test_streaming_response_delete(self, client: Hyperspell) -> None:
+ with client.memories.with_streaming_response.delete(
+ resource_id="resource_id",
+ source="collections",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ memory = response.parse()
+ assert_matches_type(MemoryDeleteResponse, memory, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ def test_path_params_delete(self, client: Hyperspell) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `resource_id` but received ''"):
+ client.memories.with_raw_response.delete(
+ resource_id="",
+ source="collections",
+ )
+
@parametrize
def test_method_add(self, client: Hyperspell) -> None:
memory = client.memories.add(
@@ -155,47 +198,65 @@ def test_method_search_with_all_params(self, client: Hyperspell) -> None:
memory = client.memories.search(
query="query",
answer=True,
- filter={
- "after": parse_datetime("2019-12-27T18:11:19.117Z"),
- "answer_model": "llama-3.1",
- "before": parse_datetime("2019-12-27T18:11:19.117Z"),
- "box": {},
- "collections": {},
- "google_calendar": {"calendar_id": "calendar_id"},
- "google_drive": {},
- "google_mail": {"label_ids": ["string"]},
- "notion": {"notion_page_ids": ["string"]},
- "reddit": {
- "period": "hour",
- "sort": "relevance",
- "subreddit": "subreddit",
- },
- "slack": {"channels": ["string"]},
- "web_crawler": {
- "max_depth": 0,
- "url": "string",
- },
- },
max_results=0,
options={
"after": parse_datetime("2019-12-27T18:11:19.117Z"),
"answer_model": "llama-3.1",
"before": parse_datetime("2019-12-27T18:11:19.117Z"),
- "box": {},
- "collections": {},
- "google_calendar": {"calendar_id": "calendar_id"},
- "google_drive": {},
- "google_mail": {"label_ids": ["string"]},
- "notion": {"notion_page_ids": ["string"]},
+ "box": {
+ "after": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "weight": 0,
+ },
+ "collections": {
+ "after": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "weight": 0,
+ },
+ "google_calendar": {
+ "after": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "calendar_id": "calendar_id",
+ "weight": 0,
+ },
+ "google_drive": {
+ "after": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "weight": 0,
+ },
+ "google_mail": {
+ "after": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "label_ids": ["string"],
+ "weight": 0,
+ },
+ "max_results": 0,
+ "notion": {
+ "after": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "notion_page_ids": ["string"],
+ "weight": 0,
+ },
"reddit": {
+ "after": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "before": parse_datetime("2019-12-27T18:11:19.117Z"),
"period": "hour",
"sort": "relevance",
"subreddit": "subreddit",
+ "weight": 0,
+ },
+ "slack": {
+ "after": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "channels": ["string"],
+ "weight": 0,
},
- "slack": {"channels": ["string"]},
"web_crawler": {
+ "after": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "before": parse_datetime("2019-12-27T18:11:19.117Z"),
"max_depth": 0,
"url": "string",
+ "weight": 0,
},
},
sources=["collections"],
@@ -331,6 +392,48 @@ async def test_streaming_response_list(self, async_client: AsyncHyperspell) -> N
assert cast(Any, response.is_closed) is True
+ @parametrize
+ async def test_method_delete(self, async_client: AsyncHyperspell) -> None:
+ memory = await async_client.memories.delete(
+ resource_id="resource_id",
+ source="collections",
+ )
+ assert_matches_type(MemoryDeleteResponse, memory, path=["response"])
+
+ @parametrize
+ async def test_raw_response_delete(self, async_client: AsyncHyperspell) -> None:
+ response = await async_client.memories.with_raw_response.delete(
+ resource_id="resource_id",
+ source="collections",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ memory = await response.parse()
+ assert_matches_type(MemoryDeleteResponse, memory, path=["response"])
+
+ @parametrize
+ async def test_streaming_response_delete(self, async_client: AsyncHyperspell) -> None:
+ async with async_client.memories.with_streaming_response.delete(
+ resource_id="resource_id",
+ source="collections",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ memory = await response.parse()
+ assert_matches_type(MemoryDeleteResponse, memory, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @parametrize
+ async def test_path_params_delete(self, async_client: AsyncHyperspell) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `resource_id` but received ''"):
+ await async_client.memories.with_raw_response.delete(
+ resource_id="",
+ source="collections",
+ )
+
@parametrize
async def test_method_add(self, async_client: AsyncHyperspell) -> None:
memory = await async_client.memories.add(
@@ -427,47 +530,65 @@ async def test_method_search_with_all_params(self, async_client: AsyncHyperspell
memory = await async_client.memories.search(
query="query",
answer=True,
- filter={
- "after": parse_datetime("2019-12-27T18:11:19.117Z"),
- "answer_model": "llama-3.1",
- "before": parse_datetime("2019-12-27T18:11:19.117Z"),
- "box": {},
- "collections": {},
- "google_calendar": {"calendar_id": "calendar_id"},
- "google_drive": {},
- "google_mail": {"label_ids": ["string"]},
- "notion": {"notion_page_ids": ["string"]},
- "reddit": {
- "period": "hour",
- "sort": "relevance",
- "subreddit": "subreddit",
- },
- "slack": {"channels": ["string"]},
- "web_crawler": {
- "max_depth": 0,
- "url": "string",
- },
- },
max_results=0,
options={
"after": parse_datetime("2019-12-27T18:11:19.117Z"),
"answer_model": "llama-3.1",
"before": parse_datetime("2019-12-27T18:11:19.117Z"),
- "box": {},
- "collections": {},
- "google_calendar": {"calendar_id": "calendar_id"},
- "google_drive": {},
- "google_mail": {"label_ids": ["string"]},
- "notion": {"notion_page_ids": ["string"]},
+ "box": {
+ "after": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "weight": 0,
+ },
+ "collections": {
+ "after": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "weight": 0,
+ },
+ "google_calendar": {
+ "after": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "calendar_id": "calendar_id",
+ "weight": 0,
+ },
+ "google_drive": {
+ "after": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "weight": 0,
+ },
+ "google_mail": {
+ "after": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "label_ids": ["string"],
+ "weight": 0,
+ },
+ "max_results": 0,
+ "notion": {
+ "after": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "notion_page_ids": ["string"],
+ "weight": 0,
+ },
"reddit": {
+ "after": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "before": parse_datetime("2019-12-27T18:11:19.117Z"),
"period": "hour",
"sort": "relevance",
"subreddit": "subreddit",
+ "weight": 0,
+ },
+ "slack": {
+ "after": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "before": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "channels": ["string"],
+ "weight": 0,
},
- "slack": {"channels": ["string"]},
"web_crawler": {
+ "after": parse_datetime("2019-12-27T18:11:19.117Z"),
+ "before": parse_datetime("2019-12-27T18:11:19.117Z"),
"max_depth": 0,
"url": "string",
+ "weight": 0,
},
},
sources=["collections"],
diff --git a/tests/test_models.py b/tests/test_models.py
index cfcb602e..0a875386 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -1,5 +1,5 @@
import json
-from typing import Any, Dict, List, Union, Optional, cast
+from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional, cast
from datetime import datetime, timezone
from typing_extensions import Literal, Annotated, TypeAliasType
@@ -934,3 +934,30 @@ class Type2(BaseModel):
)
assert isinstance(model, Type1)
assert isinstance(model.value, InnerType2)
+
+
+@pytest.mark.skipif(not PYDANTIC_V2, reason="this is only supported in pydantic v2 for now")
+def test_extra_properties() -> None:
+ class Item(BaseModel):
+ prop: int
+
+ class Model(BaseModel):
+ __pydantic_extra__: Dict[str, Item] = Field(init=False) # pyright: ignore[reportIncompatibleVariableOverride]
+
+ other: str
+
+ if TYPE_CHECKING:
+
+ def __getattr__(self, attr: str) -> Item: ...
+
+ model = construct_type(
+ type_=Model,
+ value={
+ "a": {"prop": 1},
+ "other": "foo",
+ },
+ )
+ assert isinstance(model, Model)
+ assert model.a.prop == 1
+ assert isinstance(model.a, Item)
+ assert model.other == "foo"