diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8fe4f0ac..99b8fcff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,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 steps: - uses: actions/checkout@v4 @@ -34,10 +35,10 @@ jobs: - name: Run lints run: ./scripts/lint - upload: - if: github.repository == 'stainless-sdks/hyperspell-python' + build: + if: github.repository == 'stainless-sdks/hyperspell-python' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork) timeout-minutes: 10 - name: upload + name: build permissions: contents: read id-token: write @@ -45,6 +46,20 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install Rye + run: | + curl -sSf https://rye.astral.sh/get | bash + echo "$HOME/.rye/shims" >> $GITHUB_PATH + env: + RYE_VERSION: '0.44.0' + RYE_INSTALL_OPTION: '--yes' + + - name: Install dependencies + run: rye sync --all-features + + - name: Run build + run: rye build + - name: Get GitHub OIDC Token id: github-oidc uses: actions/github-script@v6 @@ -62,6 +77,7 @@ jobs: timeout-minutes: 10 name: test runs-on: ${{ github.repository == 'stainless-sdks/hyperspell-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@v4 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4ad3fef3..e7562934 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.18.0" + ".": "0.19.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 69178cfe..2c00fa68 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-e89c764e4c847e7825e07bdb7e927a99b29548a8864069365d50974e8d4518fe.yml -openapi_spec_hash: fadd50b9f0b9675cbf5c16c318feef2f -config_hash: 32d007e1683b1936cd145cd41982ebb6 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hyperspell%2Fhyperspell-4bfa5ab6a0021f526d6f6f622f1872f2ec3467b6d1bd51acfc17bfc6f399f915.yml +openapi_spec_hash: 5b51df5010bab70a1f3f884552ce25c3 +config_hash: c894437241b21cedd2d01854f1c7a8ef diff --git a/CHANGELOG.md b/CHANGELOG.md index e1a462e9..e3ee8045 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ # Changelog +## 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) + +### Features + +* **api:** api update ([e4edf05](https://github.com/hyperspell/python-sdk/commit/e4edf05d2e2700fef1e70250204aee103ac54686)) +* **api:** api update ([7248ff5](https://github.com/hyperspell/python-sdk/commit/7248ff5db8bbc8708f8dfe8afb78d519401ee7f2)) +* **api:** update via SDK Studio ([20bf1bb](https://github.com/hyperspell/python-sdk/commit/20bf1bbf36b4b5aa984cd6f405eb98fb6a743dba)) +* **api:** update via SDK Studio ([21036b0](https://github.com/hyperspell/python-sdk/commit/21036b0a3b626fa41925b58b24616484158b9361)) +* **api:** update via SDK Studio ([bf294db](https://github.com/hyperspell/python-sdk/commit/bf294db204ddb25338ab2370d78d6c378c78686a)) +* clean up environment call outs ([e99844e](https://github.com/hyperspell/python-sdk/commit/e99844ee29928beeed51cd47d53e3ababe33e608)) +* **client:** add support for aiohttp ([c0845dc](https://github.com/hyperspell/python-sdk/commit/c0845dc8133eb74e72f18651e9da53102609d833)) + + +### Bug Fixes + +* **ci:** correct conditional ([980b910](https://github.com/hyperspell/python-sdk/commit/980b910eaa78cf84de10c0521e4ed2f142689ae0)) +* **ci:** release-doctor — report correct token name ([6ddb78b](https://github.com/hyperspell/python-sdk/commit/6ddb78b355f8e36a85fcfaf1592618bae5c67473)) +* **client:** don't send Content-Type header on GET requests ([f7a04b5](https://github.com/hyperspell/python-sdk/commit/f7a04b5a47aa9a6fc338498a34db4074824c198e)) +* **parsing:** correctly handle nested discriminated unions ([f4e309d](https://github.com/hyperspell/python-sdk/commit/f4e309d6a8d85f5aa0cb3fee3e41bf9c6487b5b1)) + + +### Chores + +* **ci:** change upload type ([c01d519](https://github.com/hyperspell/python-sdk/commit/c01d519131a2c4620c9454c4e634c2e39053b00c)) +* **ci:** only run for pushes and fork pull requests ([991d685](https://github.com/hyperspell/python-sdk/commit/991d6859286e86aa022eb83aab8ecc6e337798f4)) +* **internal:** bump pinned h11 dep ([1ce5781](https://github.com/hyperspell/python-sdk/commit/1ce57814e1d392b0c6a51b75c1dd92550f9399c3)) +* **internal:** codegen related update ([f7cd6c7](https://github.com/hyperspell/python-sdk/commit/f7cd6c7b1feafef059ecdf6be0bb9adbd5f28929)) +* **package:** mark python 3.13 as supported ([b04d1e6](https://github.com/hyperspell/python-sdk/commit/b04d1e6c7d2dbd8852963ee8f08596ed332e04ec)) +* **readme:** fix version rendering on pypi ([a109225](https://github.com/hyperspell/python-sdk/commit/a1092259c98715d17ce95259420baf3c3d325db8)) +* **tests:** skip some failing tests on the latest python versions ([9eebf62](https://github.com/hyperspell/python-sdk/commit/9eebf628b254b4f463ea7b899dcef53af5362228)) + ## 0.18.0 (2025-06-19) Full Changelog: [v0.16.0...v0.18.0](https://github.com/hyperspell/python-sdk/compare/v0.16.0...v0.18.0) diff --git a/README.md b/README.md index cc22c6ee..5890113e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Hyperspell Python API library -[![PyPI version]()](https://pypi.org/project/hyperspell/) + +[![PyPI version](https://img.shields.io/pypi/v/hyperspell.svg?label=pypi%20(stable))](https://pypi.org/project/hyperspell/) The Hyperspell Python library provides convenient access to the Hyperspell REST API from any Python 3.8+ application. The library includes type definitions for all request params and response fields, @@ -31,10 +32,10 @@ client = Hyperspell( api_key=os.environ.get("HYPERSPELL_TOKEN"), # This is the default and can be omitted ) -document_status = client.documents.add( +memory_status = client.memories.add( text="text", ) -print(document_status.id) +print(memory_status.id) ``` While you can provide an `api_key` keyword argument, @@ -57,10 +58,10 @@ client = AsyncHyperspell( async def main() -> None: - document_status = await client.documents.add( + memory_status = await client.memories.add( text="text", ) - print(document_status.id) + print(memory_status.id) asyncio.run(main()) @@ -68,6 +69,39 @@ asyncio.run(main()) Functionality between the synchronous and asynchronous clients is otherwise identical. +### With aiohttp + +By default, the async client uses `httpx` for HTTP requests. However, for improved concurrency performance you may also use `aiohttp` as the HTTP backend. + +You can enable this by installing `aiohttp`: + +```sh +# install from PyPI +pip install hyperspell[aiohttp] +``` + +Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: + +```python +import asyncio +from hyperspell import DefaultAioHttpClient +from hyperspell import AsyncHyperspell + + +async def main() -> None: + async with AsyncHyperspell( + api_key="My API Key", + http_client=DefaultAioHttpClient(), + ) as client: + memory_status = await client.memories.add( + text="text", + ) + print(memory_status.id) + + +asyncio.run(main()) +``` + ## Using types Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like: @@ -88,14 +122,14 @@ from hyperspell import Hyperspell client = Hyperspell() -all_documents = [] +all_memories = [] # Automatically fetches more pages as needed. -for document in client.documents.list( +for memory in client.memories.list( collection="REPLACE_ME", ): - # Do something with document here - all_documents.append(document) -print(all_documents) + # Do something with memory here + all_memories.append(memory) +print(all_memories) ``` Or, asynchronously: @@ -108,13 +142,13 @@ client = AsyncHyperspell() async def main() -> None: - all_documents = [] + all_memories = [] # Iterate through items across all pages, issuing requests as needed. - async for document in client.documents.list( + async for memory in client.memories.list( collection="REPLACE_ME", ): - all_documents.append(document) - print(all_documents) + all_memories.append(memory) + print(all_memories) asyncio.run(main()) @@ -123,7 +157,7 @@ asyncio.run(main()) Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages: ```python -first_page = await client.documents.list( +first_page = await client.memories.list( collection="REPLACE_ME", ) if first_page.has_next_page(): @@ -137,13 +171,13 @@ if first_page.has_next_page(): Or just work directly with the returned data: ```python -first_page = await client.documents.list( +first_page = await client.memories.list( collection="REPLACE_ME", ) print(f"next page cursor: {first_page.next_cursor}") # => "next page cursor: ..." -for document in first_page.items: - print(document.resource_id) +for memory in first_page.items: + print(memory.resource_id) # Remove `await` for non-async usage. ``` @@ -157,7 +191,7 @@ from hyperspell import Hyperspell client = Hyperspell() -response = client.query.search( +response = client.memories.search( query="query", filter={}, ) @@ -174,7 +208,7 @@ from hyperspell import Hyperspell client = Hyperspell() -client.documents.upload( +client.memories.upload( file=Path("/path/to/file"), ) ``` @@ -197,7 +231,7 @@ from hyperspell import Hyperspell client = Hyperspell() try: - client.documents.add( + client.memories.add( text="text", ) except hyperspell.APIConnectionError as e: @@ -242,7 +276,7 @@ client = Hyperspell( ) # Or, configure per-request: -client.with_options(max_retries=5).documents.add( +client.with_options(max_retries=5).memories.add( text="text", ) ``` @@ -267,7 +301,7 @@ client = Hyperspell( ) # Override per-request: -client.with_options(timeout=5.0).documents.add( +client.with_options(timeout=5.0).memories.add( text="text", ) ``` @@ -310,13 +344,13 @@ The "raw" Response object can be accessed by prefixing `.with_raw_response.` to from hyperspell import Hyperspell client = Hyperspell() -response = client.documents.with_raw_response.add( +response = client.memories.with_raw_response.add( text="text", ) print(response.headers.get('X-My-Header')) -document = response.parse() # get the object that `documents.add()` would have returned -print(document.id) +memory = response.parse() # get the object that `memories.add()` would have returned +print(memory.id) ``` These methods return an [`APIResponse`](https://github.com/hyperspell/python-sdk/tree/main/src/hyperspell/_response.py) object. @@ -330,7 +364,7 @@ The above interface eagerly reads the full response body when you make the reque To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. ```python -with client.documents.with_streaming_response.add( +with client.memories.with_streaming_response.add( text="text", ) as response: print(response.headers.get("X-My-Header")) diff --git a/api.md b/api.md index c8fcbbc2..bba49d1a 100644 --- a/api.md +++ b/api.md @@ -34,45 +34,34 @@ Methods: - client.integrations.web_crawler.index(\*\*params) -> WebCrawlerIndexResponse -# Documents +# Memories Types: ```python -from hyperspell.types import Document, DocumentStatus, DocumentStatusResponse +from hyperspell.types import Memory, MemoryStatus, MemorySearchResponse, MemoryStatusResponse ``` Methods: -- client.documents.list(\*\*params) -> SyncCursorPage[Document] -- client.documents.add(\*\*params) -> DocumentStatus -- client.documents.get(resource_id, \*, source) -> Document -- client.documents.status() -> DocumentStatusResponse -- client.documents.upload(\*\*params) -> DocumentStatus +- client.memories.list(\*\*params) -> SyncCursorPage[Memory] +- client.memories.add(\*\*params) -> MemoryStatus +- client.memories.get(resource_id, \*, source) -> Memory +- client.memories.search(\*\*params) -> MemorySearchResponse +- client.memories.status() -> MemoryStatusResponse +- client.memories.upload(\*\*params) -> MemoryStatus -# Collections +# Vaults Types: ```python -from hyperspell.types import CollectionListResponse +from hyperspell.types import VaultListResponse ``` Methods: -- client.collections.list(\*\*params) -> SyncCursorPage[CollectionListResponse] - -# Query - -Types: - -```python -from hyperspell.types import QuerySearchResponse -``` - -Methods: - -- client.query.search(\*\*params) -> QuerySearchResponse +- client.vaults.list(\*\*params) -> SyncCursorPage[VaultListResponse] # Auth diff --git a/bin/check-release-environment b/bin/check-release-environment index c9260bb3..b845b0f4 100644 --- a/bin/check-release-environment +++ b/bin/check-release-environment @@ -3,7 +3,7 @@ errors=() if [ -z "${PYPI_TOKEN}" ]; then - errors+=("The HYPERSPELL_PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") + errors+=("The PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") fi lenErrors=${#errors[@]} diff --git a/pyproject.toml b/pyproject.toml index 2c96da28..b08762c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "hyperspell" -version = "0.18.0" +version = "0.19.0" description = "The official Python library for the hyperspell API" dynamic = ["readme"] license = "Apache-2.0" @@ -24,6 +24,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: MacOS", @@ -37,6 +38,8 @@ classifiers = [ Homepage = "https://github.com/hyperspell/python-sdk" Repository = "https://github.com/hyperspell/python-sdk" +[project.optional-dependencies] +aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.8"] [tool.rye] managed = true diff --git a/requirements-dev.lock b/requirements-dev.lock index 26cd3924..271a703f 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -10,6 +10,13 @@ # universal: false -e file:. +aiohappyeyeballs==2.6.1 + # via aiohttp +aiohttp==3.12.8 + # via httpx-aiohttp + # via hyperspell +aiosignal==1.3.2 + # via aiohttp annotated-types==0.6.0 # via pydantic anyio==4.4.0 @@ -17,6 +24,10 @@ anyio==4.4.0 # via hyperspell argcomplete==3.1.2 # via nox +async-timeout==5.0.1 + # via aiohttp +attrs==25.3.0 + # via aiohttp certifi==2023.7.22 # via httpcore # via httpx @@ -34,16 +45,23 @@ execnet==2.1.1 # via pytest-xdist filelock==3.12.4 # via virtualenv -h11==0.14.0 +frozenlist==1.6.2 + # via aiohttp + # via aiosignal +h11==0.16.0 # via httpcore -httpcore==1.0.2 +httpcore==1.0.9 # via httpx httpx==0.28.1 + # via httpx-aiohttp # via hyperspell # via respx +httpx-aiohttp==0.1.8 + # via hyperspell idna==3.4 # via anyio # via httpx + # via yarl importlib-metadata==7.0.0 iniconfig==2.0.0 # via pytest @@ -51,6 +69,9 @@ markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py +multidict==6.4.4 + # via aiohttp + # via yarl mypy==1.14.1 mypy-extensions==1.0.0 # via mypy @@ -65,6 +86,9 @@ platformdirs==3.11.0 # via virtualenv pluggy==1.5.0 # via pytest +propcache==0.3.1 + # via aiohttp + # via yarl pydantic==2.10.3 # via hyperspell pydantic-core==2.27.1 @@ -98,11 +122,14 @@ tomli==2.0.2 typing-extensions==4.12.2 # via anyio # via hyperspell + # via multidict # via mypy # via pydantic # via pydantic-core # via pyright virtualenv==20.24.5 # via nox +yarl==1.20.0 + # via aiohttp zipp==3.17.0 # via importlib-metadata diff --git a/requirements.lock b/requirements.lock index 465a1c4b..c51d6551 100644 --- a/requirements.lock +++ b/requirements.lock @@ -10,11 +10,22 @@ # universal: false -e file:. +aiohappyeyeballs==2.6.1 + # via aiohttp +aiohttp==3.12.8 + # via httpx-aiohttp + # via hyperspell +aiosignal==1.3.2 + # via aiohttp annotated-types==0.6.0 # via pydantic anyio==4.4.0 # via httpx # via hyperspell +async-timeout==5.0.1 + # via aiohttp +attrs==25.3.0 + # via aiohttp certifi==2023.7.22 # via httpcore # via httpx @@ -22,15 +33,28 @@ distro==1.8.0 # via hyperspell exceptiongroup==1.2.2 # via anyio -h11==0.14.0 +frozenlist==1.6.2 + # via aiohttp + # via aiosignal +h11==0.16.0 # via httpcore -httpcore==1.0.2 +httpcore==1.0.9 # via httpx httpx==0.28.1 + # via httpx-aiohttp + # via hyperspell +httpx-aiohttp==0.1.8 # via hyperspell idna==3.4 # via anyio # via httpx + # via yarl +multidict==6.4.4 + # via aiohttp + # via yarl +propcache==0.3.1 + # via aiohttp + # via yarl pydantic==2.10.3 # via hyperspell pydantic-core==2.27.1 @@ -41,5 +65,8 @@ sniffio==1.3.0 typing-extensions==4.12.2 # via anyio # via hyperspell + # via multidict # via pydantic # via pydantic-core +yarl==1.20.0 + # via aiohttp diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh index 77a03527..5a2c4633 100755 --- a/scripts/utils/upload-artifact.sh +++ b/scripts/utils/upload-artifact.sh @@ -1,7 +1,9 @@ #!/usr/bin/env bash set -exuo pipefail -RESPONSE=$(curl -X POST "$URL" \ +FILENAME=$(basename dist/*.whl) + +RESPONSE=$(curl -X POST "$URL?filename=$FILENAME" \ -H "Authorization: Bearer $AUTH" \ -H "Content-Type: application/json") @@ -12,13 +14,13 @@ if [[ "$SIGNED_URL" == "null" ]]; then exit 1 fi -UPLOAD_RESPONSE=$(tar -cz . | curl -v -X PUT \ - -H "Content-Type: application/gzip" \ - --data-binary @- "$SIGNED_URL" 2>&1) +UPLOAD_RESPONSE=$(curl -v -X PUT \ + -H "Content-Type: binary/octet-stream" \ + --data-binary "@dist/$FILENAME" "$SIGNED_URL" 2>&1) if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then echo -e "\033[32mUploaded build to Stainless storage.\033[0m" - echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/hyperspell-python/$SHA'\033[0m" + echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/hyperspell-python/$SHA/$FILENAME'\033[0m" else echo -e "\033[31mFailed to upload artifact.\033[0m" exit 1 diff --git a/src/hyperspell/__init__.py b/src/hyperspell/__init__.py index 62f31249..3f17ad2b 100644 --- a/src/hyperspell/__init__.py +++ b/src/hyperspell/__init__.py @@ -36,7 +36,7 @@ UnprocessableEntityError, APIResponseValidationError, ) -from ._base_client import DefaultHttpxClient, DefaultAsyncHttpxClient +from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient from ._utils._logs import setup_logging as _setup_logging __all__ = [ @@ -78,6 +78,7 @@ "DEFAULT_CONNECTION_LIMITS", "DefaultHttpxClient", "DefaultAsyncHttpxClient", + "DefaultAioHttpClient", ] if not _t.TYPE_CHECKING: diff --git a/src/hyperspell/_base_client.py b/src/hyperspell/_base_client.py index dffdfe10..93ac9ef9 100644 --- a/src/hyperspell/_base_client.py +++ b/src/hyperspell/_base_client.py @@ -529,6 +529,15 @@ def _build_request( # work around https://github.com/encode/httpx/discussions/2880 kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} + is_body_allowed = options.method.lower() != "get" + + if is_body_allowed: + kwargs["json"] = json_data if is_given(json_data) else None + kwargs["files"] = files + else: + headers.pop("Content-Type", None) + kwargs.pop("data", None) + # TODO: report this error to httpx return self._client.build_request( # pyright: ignore[reportUnknownMemberType] headers=headers, @@ -540,8 +549,6 @@ def _build_request( # so that passing a `TypedDict` doesn't cause an error. # https://github.com/microsoft/pyright/issues/3526#event-6715453066 params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None, - json=json_data if is_given(json_data) else None, - files=files, **kwargs, ) @@ -1289,6 +1296,24 @@ def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) +try: + import httpx_aiohttp +except ImportError: + + class _DefaultAioHttpClient(httpx.AsyncClient): + def __init__(self, **_kwargs: Any) -> None: + raise RuntimeError("To use the aiohttp client you must have installed the package with the `aiohttp` extra") +else: + + class _DefaultAioHttpClient(httpx_aiohttp.HttpxAiohttpClient): # type: ignore + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("timeout", DEFAULT_TIMEOUT) + kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) + kwargs.setdefault("follow_redirects", True) + + super().__init__(**kwargs) + + if TYPE_CHECKING: DefaultAsyncHttpxClient = httpx.AsyncClient """An alias to `httpx.AsyncClient` that provides the same defaults that this SDK @@ -1297,8 +1322,12 @@ def __init__(self, **kwargs: Any) -> None: This is useful because overriding the `http_client` with your own instance of `httpx.AsyncClient` will result in httpx's defaults being used, not ours. """ + + DefaultAioHttpClient = httpx.AsyncClient + """An alias to `httpx.AsyncClient` that changes the default HTTP transport to `aiohttp`.""" else: DefaultAsyncHttpxClient = _DefaultAsyncHttpxClient + DefaultAioHttpClient = _DefaultAioHttpClient class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient): diff --git a/src/hyperspell/_client.py b/src/hyperspell/_client.py index 1fa37b21..4c5bd535 100644 --- a/src/hyperspell/_client.py +++ b/src/hyperspell/_client.py @@ -21,7 +21,7 @@ ) from ._utils import is_given, get_async_library from ._version import __version__ -from .resources import auth, query, documents, collections +from .resources import auth, vaults, memories from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import APIStatusError, HyperspellError from ._base_client import ( @@ -45,9 +45,8 @@ class Hyperspell(SyncAPIClient): integrations: integrations.IntegrationsResource - documents: documents.DocumentsResource - collections: collections.CollectionsResource - query: query.QueryResource + memories: memories.MemoriesResource + vaults: vaults.VaultsResource auth: auth.AuthResource with_raw_response: HyperspellWithRawResponse with_streaming_response: HyperspellWithStreamedResponse @@ -111,9 +110,8 @@ def __init__( ) self.integrations = integrations.IntegrationsResource(self) - self.documents = documents.DocumentsResource(self) - self.collections = collections.CollectionsResource(self) - self.query = query.QueryResource(self) + self.memories = memories.MemoriesResource(self) + self.vaults = vaults.VaultsResource(self) self.auth = auth.AuthResource(self) self.with_raw_response = HyperspellWithRawResponse(self) self.with_streaming_response = HyperspellWithStreamedResponse(self) @@ -238,9 +236,8 @@ def _make_status_error( class AsyncHyperspell(AsyncAPIClient): integrations: integrations.AsyncIntegrationsResource - documents: documents.AsyncDocumentsResource - collections: collections.AsyncCollectionsResource - query: query.AsyncQueryResource + memories: memories.AsyncMemoriesResource + vaults: vaults.AsyncVaultsResource auth: auth.AsyncAuthResource with_raw_response: AsyncHyperspellWithRawResponse with_streaming_response: AsyncHyperspellWithStreamedResponse @@ -304,9 +301,8 @@ def __init__( ) self.integrations = integrations.AsyncIntegrationsResource(self) - self.documents = documents.AsyncDocumentsResource(self) - self.collections = collections.AsyncCollectionsResource(self) - self.query = query.AsyncQueryResource(self) + self.memories = memories.AsyncMemoriesResource(self) + self.vaults = vaults.AsyncVaultsResource(self) self.auth = auth.AsyncAuthResource(self) self.with_raw_response = AsyncHyperspellWithRawResponse(self) self.with_streaming_response = AsyncHyperspellWithStreamedResponse(self) @@ -432,36 +428,32 @@ def _make_status_error( class HyperspellWithRawResponse: def __init__(self, client: Hyperspell) -> None: self.integrations = integrations.IntegrationsResourceWithRawResponse(client.integrations) - self.documents = documents.DocumentsResourceWithRawResponse(client.documents) - self.collections = collections.CollectionsResourceWithRawResponse(client.collections) - self.query = query.QueryResourceWithRawResponse(client.query) + self.memories = memories.MemoriesResourceWithRawResponse(client.memories) + self.vaults = vaults.VaultsResourceWithRawResponse(client.vaults) self.auth = auth.AuthResourceWithRawResponse(client.auth) class AsyncHyperspellWithRawResponse: def __init__(self, client: AsyncHyperspell) -> None: self.integrations = integrations.AsyncIntegrationsResourceWithRawResponse(client.integrations) - self.documents = documents.AsyncDocumentsResourceWithRawResponse(client.documents) - self.collections = collections.AsyncCollectionsResourceWithRawResponse(client.collections) - self.query = query.AsyncQueryResourceWithRawResponse(client.query) + self.memories = memories.AsyncMemoriesResourceWithRawResponse(client.memories) + self.vaults = vaults.AsyncVaultsResourceWithRawResponse(client.vaults) self.auth = auth.AsyncAuthResourceWithRawResponse(client.auth) class HyperspellWithStreamedResponse: def __init__(self, client: Hyperspell) -> None: self.integrations = integrations.IntegrationsResourceWithStreamingResponse(client.integrations) - self.documents = documents.DocumentsResourceWithStreamingResponse(client.documents) - self.collections = collections.CollectionsResourceWithStreamingResponse(client.collections) - self.query = query.QueryResourceWithStreamingResponse(client.query) + self.memories = memories.MemoriesResourceWithStreamingResponse(client.memories) + self.vaults = vaults.VaultsResourceWithStreamingResponse(client.vaults) self.auth = auth.AuthResourceWithStreamingResponse(client.auth) class AsyncHyperspellWithStreamedResponse: def __init__(self, client: AsyncHyperspell) -> None: self.integrations = integrations.AsyncIntegrationsResourceWithStreamingResponse(client.integrations) - self.documents = documents.AsyncDocumentsResourceWithStreamingResponse(client.documents) - self.collections = collections.AsyncCollectionsResourceWithStreamingResponse(client.collections) - self.query = query.AsyncQueryResourceWithStreamingResponse(client.query) + self.memories = memories.AsyncMemoriesResourceWithStreamingResponse(client.memories) + self.vaults = vaults.AsyncVaultsResourceWithStreamingResponse(client.vaults) self.auth = auth.AsyncAuthResourceWithStreamingResponse(client.auth) diff --git a/src/hyperspell/_models.py b/src/hyperspell/_models.py index 4f214980..528d5680 100644 --- a/src/hyperspell/_models.py +++ b/src/hyperspell/_models.py @@ -2,9 +2,10 @@ import os import inspect -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast +from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast from datetime import date, datetime from typing_extensions import ( + List, Unpack, Literal, ClassVar, @@ -366,7 +367,7 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: if type_ is None: raise RuntimeError(f"Unexpected field type is None for {key}") - return construct_type(value=value, type_=type_) + return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None)) def is_basemodel(type_: type) -> bool: @@ -420,7 +421,7 @@ def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T: return cast(_T, construct_type(value=value, type_=type_)) -def construct_type(*, value: object, type_: object) -> object: +def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]] = None) -> object: """Loose coercion to the expected type with construction of nested values. If the given value does not match the expected type then it is returned as-is. @@ -438,8 +439,10 @@ def construct_type(*, value: object, type_: object) -> object: type_ = type_.__value__ # type: ignore[unreachable] # unwrap `Annotated[T, ...]` -> `T` - if is_annotated_type(type_): - meta: tuple[Any, ...] = get_args(type_)[1:] + if metadata is not None: + meta: tuple[Any, ...] = tuple(metadata) + elif is_annotated_type(type_): + meta = get_args(type_)[1:] type_ = extract_type_arg(type_, 0) else: meta = tuple() diff --git a/src/hyperspell/_version.py b/src/hyperspell/_version.py index df0b6d8e..60454a0d 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.18.0" # x-release-please-version +__version__ = "0.19.0" # x-release-please-version diff --git a/src/hyperspell/resources/__init__.py b/src/hyperspell/resources/__init__.py index 35893fbf..71ddb874 100644 --- a/src/hyperspell/resources/__init__.py +++ b/src/hyperspell/resources/__init__.py @@ -8,29 +8,21 @@ AuthResourceWithStreamingResponse, AsyncAuthResourceWithStreamingResponse, ) -from .query import ( - QueryResource, - AsyncQueryResource, - QueryResourceWithRawResponse, - AsyncQueryResourceWithRawResponse, - QueryResourceWithStreamingResponse, - AsyncQueryResourceWithStreamingResponse, +from .vaults import ( + VaultsResource, + AsyncVaultsResource, + VaultsResourceWithRawResponse, + AsyncVaultsResourceWithRawResponse, + VaultsResourceWithStreamingResponse, + AsyncVaultsResourceWithStreamingResponse, ) -from .documents import ( - DocumentsResource, - AsyncDocumentsResource, - DocumentsResourceWithRawResponse, - AsyncDocumentsResourceWithRawResponse, - DocumentsResourceWithStreamingResponse, - AsyncDocumentsResourceWithStreamingResponse, -) -from .collections import ( - CollectionsResource, - AsyncCollectionsResource, - CollectionsResourceWithRawResponse, - AsyncCollectionsResourceWithRawResponse, - CollectionsResourceWithStreamingResponse, - AsyncCollectionsResourceWithStreamingResponse, +from .memories import ( + MemoriesResource, + AsyncMemoriesResource, + MemoriesResourceWithRawResponse, + AsyncMemoriesResourceWithRawResponse, + MemoriesResourceWithStreamingResponse, + AsyncMemoriesResourceWithStreamingResponse, ) from .integrations import ( IntegrationsResource, @@ -48,24 +40,18 @@ "AsyncIntegrationsResourceWithRawResponse", "IntegrationsResourceWithStreamingResponse", "AsyncIntegrationsResourceWithStreamingResponse", - "DocumentsResource", - "AsyncDocumentsResource", - "DocumentsResourceWithRawResponse", - "AsyncDocumentsResourceWithRawResponse", - "DocumentsResourceWithStreamingResponse", - "AsyncDocumentsResourceWithStreamingResponse", - "CollectionsResource", - "AsyncCollectionsResource", - "CollectionsResourceWithRawResponse", - "AsyncCollectionsResourceWithRawResponse", - "CollectionsResourceWithStreamingResponse", - "AsyncCollectionsResourceWithStreamingResponse", - "QueryResource", - "AsyncQueryResource", - "QueryResourceWithRawResponse", - "AsyncQueryResourceWithRawResponse", - "QueryResourceWithStreamingResponse", - "AsyncQueryResourceWithStreamingResponse", + "MemoriesResource", + "AsyncMemoriesResource", + "MemoriesResourceWithRawResponse", + "AsyncMemoriesResourceWithRawResponse", + "MemoriesResourceWithStreamingResponse", + "AsyncMemoriesResourceWithStreamingResponse", + "VaultsResource", + "AsyncVaultsResource", + "VaultsResourceWithRawResponse", + "AsyncVaultsResourceWithRawResponse", + "VaultsResourceWithStreamingResponse", + "AsyncVaultsResourceWithStreamingResponse", "AuthResource", "AsyncAuthResource", "AuthResourceWithRawResponse", diff --git a/src/hyperspell/resources/integrations/integrations.py b/src/hyperspell/resources/integrations/integrations.py index 3bdd9529..d4e299e0 100644 --- a/src/hyperspell/resources/integrations/integrations.py +++ b/src/hyperspell/resources/integrations/integrations.py @@ -76,7 +76,7 @@ def revoke( ) -> IntegrationRevokeResponse: """ Revokes Hyperspell's access the given provider and deletes all stored - credentials. It does not delete any cached or synced data. + credentials and indexed data. Args: extra_headers: Send extra headers @@ -139,7 +139,7 @@ async def revoke( ) -> IntegrationRevokeResponse: """ Revokes Hyperspell's access the given provider and deletes all stored - credentials. It does not delete any cached or synced data. + credentials and indexed data. Args: extra_headers: Send extra headers diff --git a/src/hyperspell/resources/documents.py b/src/hyperspell/resources/memories.py similarity index 68% rename from src/hyperspell/resources/documents.py rename to src/hyperspell/resources/memories.py index 5d629445..94ed512e 100644 --- a/src/hyperspell/resources/documents.py +++ b/src/hyperspell/resources/memories.py @@ -2,13 +2,13 @@ from __future__ import annotations -from typing import Union, Mapping, Optional, cast +from typing import List, Union, Mapping, Optional, cast from datetime import datetime from typing_extensions import Literal import httpx -from ..types import document_add_params, document_list_params, document_upload_params +from ..types import memory_add_params, memory_list_params, memory_search_params, memory_upload_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform from .._compat import cached_property @@ -21,32 +21,33 @@ ) from ..pagination import SyncCursorPage, AsyncCursorPage from .._base_client import AsyncPaginator, make_request_options -from ..types.document import Document -from ..types.document_status import DocumentStatus -from ..types.document_status_response import DocumentStatusResponse +from ..types.memory import Memory +from ..types.memory_status import MemoryStatus +from ..types.memory_search_response import MemorySearchResponse +from ..types.memory_status_response import MemoryStatusResponse -__all__ = ["DocumentsResource", "AsyncDocumentsResource"] +__all__ = ["MemoriesResource", "AsyncMemoriesResource"] -class DocumentsResource(SyncAPIResource): +class MemoriesResource(SyncAPIResource): @cached_property - def with_raw_response(self) -> DocumentsResourceWithRawResponse: + def with_raw_response(self) -> MemoriesResourceWithRawResponse: """ This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/hyperspell/python-sdk#accessing-raw-response-data-eg-headers """ - return DocumentsResourceWithRawResponse(self) + return MemoriesResourceWithRawResponse(self) @cached_property - def with_streaming_response(self) -> DocumentsResourceWithStreamingResponse: + def with_streaming_response(self) -> MemoriesResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. For more information, see https://www.github.com/hyperspell/python-sdk#with_streaming_response """ - return DocumentsResourceWithStreamingResponse(self) + return MemoriesResourceWithStreamingResponse(self) def list( self, @@ -57,6 +58,7 @@ def list( source: Optional[ Literal[ "collections", + "vault", "web_crawler", "notion", "slack", @@ -110,7 +112,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SyncCursorPage[Document]: + ) -> SyncCursorPage[Memory]: """This endpoint allows you to paginate through all documents in the index. You can @@ -130,8 +132,8 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ return self._get_api_list( - "/documents/list", - page=SyncCursorPage[Document], + "/memories/list", + page=SyncCursorPage[Memory], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -144,10 +146,10 @@ def list( "size": size, "source": source, }, - document_list_params.DocumentListParams, + memory_list_params.MemoryListParams, ), ), - model=Document, + model=Memory, ) def add( @@ -164,7 +166,7 @@ def add( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> DocumentStatus: + ) -> MemoryStatus: """Adds an arbitrary document to the index. This can be any text, email, call @@ -195,7 +197,7 @@ def add( timeout: Override the client-level default timeout for this request, in seconds """ return self._post( - "/documents/add", + "/memories/add", body=maybe_transform( { "text": text, @@ -204,12 +206,12 @@ def add( "resource_id": resource_id, "title": title, }, - document_add_params.DocumentAddParams, + memory_add_params.MemoryAddParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=DocumentStatus, + cast_to=MemoryStatus, ) def get( @@ -218,6 +220,7 @@ def get( *, source: Literal[ "collections", + "vault", "web_crawler", "notion", "slack", @@ -269,7 +272,7 @@ def get( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Document: + ) -> Memory: """ Retrieves a document by provider and resource_id. @@ -287,11 +290,120 @@ 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"/documents/get/{source}/{resource_id}", + f"/memories/get/{source}/{resource_id}", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Document, + cast_to=Memory, + ) + + def search( + self, + *, + 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[ + 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", + ] + ] + | NotGiven = NOT_GIVEN, + # 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, + ) -> MemorySearchResponse: + """ + Retrieves documents matching the query. + + Args: + query: Query to run. + + 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. + + sources: Only query documents from these sources. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/memories/query", + body=maybe_transform( + { + "query": query, + "answer": answer, + "filter": filter, + "max_results": max_results, + "options": options, + "sources": sources, + }, + memory_search_params.MemorySearchParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MemorySearchResponse, ) def status( @@ -303,17 +415,17 @@ def status( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> DocumentStatusResponse: + ) -> MemoryStatusResponse: """ This endpoint shows the indexing progress of documents, both by provider and total. """ return self._get( - "/documents/status", + "/memories/status", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=DocumentStatusResponse, + cast_to=MemoryStatusResponse, ) def upload( @@ -327,7 +439,7 @@ def upload( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> DocumentStatus: + ) -> MemoryStatus: """This endpoint will upload a file to the index and return a document ID. The file @@ -360,35 +472,35 @@ def upload( # multipart/form-data; boundary=---abc-- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} return self._post( - "/documents/upload", - body=maybe_transform(body, document_upload_params.DocumentUploadParams), + "/memories/upload", + 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 ), - cast_to=DocumentStatus, + cast_to=MemoryStatus, ) -class AsyncDocumentsResource(AsyncAPIResource): +class AsyncMemoriesResource(AsyncAPIResource): @cached_property - def with_raw_response(self) -> AsyncDocumentsResourceWithRawResponse: + def with_raw_response(self) -> AsyncMemoriesResourceWithRawResponse: """ This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/hyperspell/python-sdk#accessing-raw-response-data-eg-headers """ - return AsyncDocumentsResourceWithRawResponse(self) + return AsyncMemoriesResourceWithRawResponse(self) @cached_property - def with_streaming_response(self) -> AsyncDocumentsResourceWithStreamingResponse: + def with_streaming_response(self) -> AsyncMemoriesResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. For more information, see https://www.github.com/hyperspell/python-sdk#with_streaming_response """ - return AsyncDocumentsResourceWithStreamingResponse(self) + return AsyncMemoriesResourceWithStreamingResponse(self) def list( self, @@ -399,6 +511,7 @@ def list( source: Optional[ Literal[ "collections", + "vault", "web_crawler", "notion", "slack", @@ -452,7 +565,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncPaginator[Document, AsyncCursorPage[Document]]: + ) -> AsyncPaginator[Memory, AsyncCursorPage[Memory]]: """This endpoint allows you to paginate through all documents in the index. You can @@ -472,8 +585,8 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ return self._get_api_list( - "/documents/list", - page=AsyncCursorPage[Document], + "/memories/list", + page=AsyncCursorPage[Memory], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -486,10 +599,10 @@ def list( "size": size, "source": source, }, - document_list_params.DocumentListParams, + memory_list_params.MemoryListParams, ), ), - model=Document, + model=Memory, ) async def add( @@ -506,7 +619,7 @@ async def add( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> DocumentStatus: + ) -> MemoryStatus: """Adds an arbitrary document to the index. This can be any text, email, call @@ -537,7 +650,7 @@ async def add( timeout: Override the client-level default timeout for this request, in seconds """ return await self._post( - "/documents/add", + "/memories/add", body=await async_maybe_transform( { "text": text, @@ -546,12 +659,12 @@ async def add( "resource_id": resource_id, "title": title, }, - document_add_params.DocumentAddParams, + memory_add_params.MemoryAddParams, ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=DocumentStatus, + cast_to=MemoryStatus, ) async def get( @@ -560,6 +673,7 @@ async def get( *, source: Literal[ "collections", + "vault", "web_crawler", "notion", "slack", @@ -611,7 +725,7 @@ async def get( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Document: + ) -> Memory: """ Retrieves a document by provider and resource_id. @@ -629,11 +743,120 @@ 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"/documents/get/{source}/{resource_id}", + f"/memories/get/{source}/{resource_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Memory, + ) + + async def search( + self, + *, + 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[ + 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", + ] + ] + | NotGiven = NOT_GIVEN, + # 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, + ) -> MemorySearchResponse: + """ + Retrieves documents matching the query. + + Args: + query: Query to run. + + 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. + + sources: Only query documents from these sources. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/memories/query", + body=await async_maybe_transform( + { + "query": query, + "answer": answer, + "filter": filter, + "max_results": max_results, + "options": options, + "sources": sources, + }, + memory_search_params.MemorySearchParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=Document, + cast_to=MemorySearchResponse, ) async def status( @@ -645,17 +868,17 @@ async def status( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> DocumentStatusResponse: + ) -> MemoryStatusResponse: """ This endpoint shows the indexing progress of documents, both by provider and total. """ return await self._get( - "/documents/status", + "/memories/status", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=DocumentStatusResponse, + cast_to=MemoryStatusResponse, ) async def upload( @@ -669,7 +892,7 @@ async def upload( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> DocumentStatus: + ) -> MemoryStatus: """This endpoint will upload a file to the index and return a document ID. The file @@ -702,95 +925,107 @@ async def upload( # multipart/form-data; boundary=---abc-- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} return await self._post( - "/documents/upload", - body=await async_maybe_transform(body, document_upload_params.DocumentUploadParams), + "/memories/upload", + 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 ), - cast_to=DocumentStatus, + cast_to=MemoryStatus, ) -class DocumentsResourceWithRawResponse: - def __init__(self, documents: DocumentsResource) -> None: - self._documents = documents +class MemoriesResourceWithRawResponse: + def __init__(self, memories: MemoriesResource) -> None: + self._memories = memories self.list = to_raw_response_wrapper( - documents.list, + memories.list, ) self.add = to_raw_response_wrapper( - documents.add, + memories.add, ) self.get = to_raw_response_wrapper( - documents.get, + memories.get, + ) + self.search = to_raw_response_wrapper( + memories.search, ) self.status = to_raw_response_wrapper( - documents.status, + memories.status, ) self.upload = to_raw_response_wrapper( - documents.upload, + memories.upload, ) -class AsyncDocumentsResourceWithRawResponse: - def __init__(self, documents: AsyncDocumentsResource) -> None: - self._documents = documents +class AsyncMemoriesResourceWithRawResponse: + def __init__(self, memories: AsyncMemoriesResource) -> None: + self._memories = memories self.list = async_to_raw_response_wrapper( - documents.list, + memories.list, ) self.add = async_to_raw_response_wrapper( - documents.add, + memories.add, ) self.get = async_to_raw_response_wrapper( - documents.get, + memories.get, + ) + self.search = async_to_raw_response_wrapper( + memories.search, ) self.status = async_to_raw_response_wrapper( - documents.status, + memories.status, ) self.upload = async_to_raw_response_wrapper( - documents.upload, + memories.upload, ) -class DocumentsResourceWithStreamingResponse: - def __init__(self, documents: DocumentsResource) -> None: - self._documents = documents +class MemoriesResourceWithStreamingResponse: + def __init__(self, memories: MemoriesResource) -> None: + self._memories = memories self.list = to_streamed_response_wrapper( - documents.list, + memories.list, ) self.add = to_streamed_response_wrapper( - documents.add, + memories.add, ) self.get = to_streamed_response_wrapper( - documents.get, + memories.get, + ) + self.search = to_streamed_response_wrapper( + memories.search, ) self.status = to_streamed_response_wrapper( - documents.status, + memories.status, ) self.upload = to_streamed_response_wrapper( - documents.upload, + memories.upload, ) -class AsyncDocumentsResourceWithStreamingResponse: - def __init__(self, documents: AsyncDocumentsResource) -> None: - self._documents = documents +class AsyncMemoriesResourceWithStreamingResponse: + def __init__(self, memories: AsyncMemoriesResource) -> None: + self._memories = memories self.list = async_to_streamed_response_wrapper( - documents.list, + memories.list, ) self.add = async_to_streamed_response_wrapper( - documents.add, + memories.add, ) self.get = async_to_streamed_response_wrapper( - documents.get, + memories.get, + ) + self.search = async_to_streamed_response_wrapper( + memories.search, ) self.status = async_to_streamed_response_wrapper( - documents.status, + memories.status, ) self.upload = async_to_streamed_response_wrapper( - documents.upload, + memories.upload, ) diff --git a/src/hyperspell/resources/query.py b/src/hyperspell/resources/query.py deleted file mode 100644 index 3b8d4533..00000000 --- a/src/hyperspell/resources/query.py +++ /dev/null @@ -1,318 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import List, Optional -from typing_extensions import Literal - -import httpx - -from ..types import query_search_params -from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven -from .._utils import maybe_transform, async_maybe_transform -from .._compat import cached_property -from .._resource import SyncAPIResource, AsyncAPIResource -from .._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from .._base_client import make_request_options -from ..types.query_search_response import QuerySearchResponse - -__all__ = ["QueryResource", "AsyncQueryResource"] - - -class QueryResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> QueryResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/hyperspell/python-sdk#accessing-raw-response-data-eg-headers - """ - return QueryResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> QueryResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/hyperspell/python-sdk#with_streaming_response - """ - return QueryResourceWithStreamingResponse(self) - - def search( - self, - *, - query: str, - answer: bool | NotGiven = NOT_GIVEN, - filter: Optional[query_search_params.Filter] | NotGiven = NOT_GIVEN, - max_results: int | NotGiven = NOT_GIVEN, - options: query_search_params.Options | NotGiven = NOT_GIVEN, - sources: List[ - Literal[ - "collections", - "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", - ] - ] - | NotGiven = NOT_GIVEN, - # 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, - ) -> QuerySearchResponse: - """ - Retrieves documents matching the query. - - Args: - query: Query to run. - - 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. - - sources: Only query documents from these sources. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return self._post( - "/query", - body=maybe_transform( - { - "query": query, - "answer": answer, - "filter": filter, - "max_results": max_results, - "options": options, - "sources": sources, - }, - query_search_params.QuerySearchParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=QuerySearchResponse, - ) - - -class AsyncQueryResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncQueryResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/hyperspell/python-sdk#accessing-raw-response-data-eg-headers - """ - return AsyncQueryResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncQueryResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/hyperspell/python-sdk#with_streaming_response - """ - return AsyncQueryResourceWithStreamingResponse(self) - - async def search( - self, - *, - query: str, - answer: bool | NotGiven = NOT_GIVEN, - filter: Optional[query_search_params.Filter] | NotGiven = NOT_GIVEN, - max_results: int | NotGiven = NOT_GIVEN, - options: query_search_params.Options | NotGiven = NOT_GIVEN, - sources: List[ - Literal[ - "collections", - "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", - ] - ] - | NotGiven = NOT_GIVEN, - # 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, - ) -> QuerySearchResponse: - """ - Retrieves documents matching the query. - - Args: - query: Query to run. - - 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. - - sources: Only query documents from these sources. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - return await self._post( - "/query", - body=await async_maybe_transform( - { - "query": query, - "answer": answer, - "filter": filter, - "max_results": max_results, - "options": options, - "sources": sources, - }, - query_search_params.QuerySearchParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=QuerySearchResponse, - ) - - -class QueryResourceWithRawResponse: - def __init__(self, query: QueryResource) -> None: - self._query = query - - self.search = to_raw_response_wrapper( - query.search, - ) - - -class AsyncQueryResourceWithRawResponse: - def __init__(self, query: AsyncQueryResource) -> None: - self._query = query - - self.search = async_to_raw_response_wrapper( - query.search, - ) - - -class QueryResourceWithStreamingResponse: - def __init__(self, query: QueryResource) -> None: - self._query = query - - self.search = to_streamed_response_wrapper( - query.search, - ) - - -class AsyncQueryResourceWithStreamingResponse: - def __init__(self, query: AsyncQueryResource) -> None: - self._query = query - - self.search = async_to_streamed_response_wrapper( - query.search, - ) diff --git a/src/hyperspell/resources/collections.py b/src/hyperspell/resources/vaults.py similarity index 70% rename from src/hyperspell/resources/collections.py rename to src/hyperspell/resources/vaults.py index e0daa155..babb8a04 100644 --- a/src/hyperspell/resources/collections.py +++ b/src/hyperspell/resources/vaults.py @@ -6,7 +6,7 @@ import httpx -from ..types import collection_list_params +from ..types import vault_list_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven from .._utils import maybe_transform from .._compat import cached_property @@ -19,30 +19,30 @@ ) from ..pagination import SyncCursorPage, AsyncCursorPage from .._base_client import AsyncPaginator, make_request_options -from ..types.collection_list_response import CollectionListResponse +from ..types.vault_list_response import VaultListResponse -__all__ = ["CollectionsResource", "AsyncCollectionsResource"] +__all__ = ["VaultsResource", "AsyncVaultsResource"] -class CollectionsResource(SyncAPIResource): +class VaultsResource(SyncAPIResource): @cached_property - def with_raw_response(self) -> CollectionsResourceWithRawResponse: + def with_raw_response(self) -> VaultsResourceWithRawResponse: """ This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/hyperspell/python-sdk#accessing-raw-response-data-eg-headers """ - return CollectionsResourceWithRawResponse(self) + return VaultsResourceWithRawResponse(self) @cached_property - def with_streaming_response(self) -> CollectionsResourceWithStreamingResponse: + def with_streaming_response(self) -> VaultsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. For more information, see https://www.github.com/hyperspell/python-sdk#with_streaming_response """ - return CollectionsResourceWithStreamingResponse(self) + return VaultsResourceWithStreamingResponse(self) def list( self, @@ -55,7 +55,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SyncCursorPage[CollectionListResponse]: + ) -> SyncCursorPage[VaultListResponse]: """ This endpoint lists all collections, and how many documents are in each collection. All documents that do not have a collection assigned are in the @@ -71,8 +71,8 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ return self._get_api_list( - "/collections/list", - page=SyncCursorPage[CollectionListResponse], + "/vault/list", + page=SyncCursorPage[VaultListResponse], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -83,32 +83,32 @@ def list( "cursor": cursor, "size": size, }, - collection_list_params.CollectionListParams, + vault_list_params.VaultListParams, ), ), - model=CollectionListResponse, + model=VaultListResponse, ) -class AsyncCollectionsResource(AsyncAPIResource): +class AsyncVaultsResource(AsyncAPIResource): @cached_property - def with_raw_response(self) -> AsyncCollectionsResourceWithRawResponse: + def with_raw_response(self) -> AsyncVaultsResourceWithRawResponse: """ This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/hyperspell/python-sdk#accessing-raw-response-data-eg-headers """ - return AsyncCollectionsResourceWithRawResponse(self) + return AsyncVaultsResourceWithRawResponse(self) @cached_property - def with_streaming_response(self) -> AsyncCollectionsResourceWithStreamingResponse: + def with_streaming_response(self) -> AsyncVaultsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. For more information, see https://www.github.com/hyperspell/python-sdk#with_streaming_response """ - return AsyncCollectionsResourceWithStreamingResponse(self) + return AsyncVaultsResourceWithStreamingResponse(self) def list( self, @@ -121,7 +121,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncPaginator[CollectionListResponse, AsyncCursorPage[CollectionListResponse]]: + ) -> AsyncPaginator[VaultListResponse, AsyncCursorPage[VaultListResponse]]: """ This endpoint lists all collections, and how many documents are in each collection. All documents that do not have a collection assigned are in the @@ -137,8 +137,8 @@ def list( timeout: Override the client-level default timeout for this request, in seconds """ return self._get_api_list( - "/collections/list", - page=AsyncCursorPage[CollectionListResponse], + "/vault/list", + page=AsyncCursorPage[VaultListResponse], options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -149,44 +149,44 @@ def list( "cursor": cursor, "size": size, }, - collection_list_params.CollectionListParams, + vault_list_params.VaultListParams, ), ), - model=CollectionListResponse, + model=VaultListResponse, ) -class CollectionsResourceWithRawResponse: - def __init__(self, collections: CollectionsResource) -> None: - self._collections = collections +class VaultsResourceWithRawResponse: + def __init__(self, vaults: VaultsResource) -> None: + self._vaults = vaults self.list = to_raw_response_wrapper( - collections.list, + vaults.list, ) -class AsyncCollectionsResourceWithRawResponse: - def __init__(self, collections: AsyncCollectionsResource) -> None: - self._collections = collections +class AsyncVaultsResourceWithRawResponse: + def __init__(self, vaults: AsyncVaultsResource) -> None: + self._vaults = vaults self.list = async_to_raw_response_wrapper( - collections.list, + vaults.list, ) -class CollectionsResourceWithStreamingResponse: - def __init__(self, collections: CollectionsResource) -> None: - self._collections = collections +class VaultsResourceWithStreamingResponse: + def __init__(self, vaults: VaultsResource) -> None: + self._vaults = vaults self.list = to_streamed_response_wrapper( - collections.list, + vaults.list, ) -class AsyncCollectionsResourceWithStreamingResponse: - def __init__(self, collections: AsyncCollectionsResource) -> None: - self._collections = collections +class AsyncVaultsResourceWithStreamingResponse: + def __init__(self, vaults: AsyncVaultsResource) -> None: + self._vaults = vaults self.list = async_to_streamed_response_wrapper( - collections.list, + vaults.list, ) diff --git a/src/hyperspell/types/__init__.py b/src/hyperspell/types/__init__.py index 4027566b..bfaf01aa 100644 --- a/src/hyperspell/types/__init__.py +++ b/src/hyperspell/types/__init__.py @@ -3,16 +3,16 @@ from __future__ import annotations from .token import Token as Token -from .document import Document as Document -from .document_status import DocumentStatus as DocumentStatus +from .memory import Memory as Memory +from .memory_status import MemoryStatus as MemoryStatus from .auth_me_response import AuthMeResponse as AuthMeResponse -from .document_add_params import DocumentAddParams as DocumentAddParams -from .query_search_params import QuerySearchParams as QuerySearchParams -from .document_list_params import DocumentListParams as DocumentListParams -from .query_search_response import QuerySearchResponse as QuerySearchResponse +from .memory_add_params import MemoryAddParams as MemoryAddParams +from .vault_list_params import VaultListParams as VaultListParams +from .memory_list_params import MemoryListParams as MemoryListParams +from .vault_list_response import VaultListResponse as VaultListResponse +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 .collection_list_params import CollectionListParams as CollectionListParams -from .document_upload_params import DocumentUploadParams as DocumentUploadParams -from .collection_list_response import CollectionListResponse as CollectionListResponse -from .document_status_response import DocumentStatusResponse as DocumentStatusResponse +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/auth_me_response.py b/src/hyperspell/types/auth_me_response.py index 10603ffe..64acb118 100644 --- a/src/hyperspell/types/auth_me_response.py +++ b/src/hyperspell/types/auth_me_response.py @@ -19,6 +19,7 @@ class AuthMeResponse(BaseModel): available_integrations: List[ Literal[ "collections", + "vault", "web_crawler", "notion", "slack", @@ -70,6 +71,7 @@ class AuthMeResponse(BaseModel): installed_integrations: List[ Literal[ "collections", + "vault", "web_crawler", "notion", "slack", diff --git a/src/hyperspell/types/integrations/web_crawler_index_response.py b/src/hyperspell/types/integrations/web_crawler_index_response.py index e5af96ce..deddf6e7 100644 --- a/src/hyperspell/types/integrations/web_crawler_index_response.py +++ b/src/hyperspell/types/integrations/web_crawler_index_response.py @@ -12,6 +12,7 @@ class WebCrawlerIndexResponse(BaseModel): source: Literal[ "collections", + "vault", "web_crawler", "notion", "slack", diff --git a/src/hyperspell/types/document.py b/src/hyperspell/types/memory.py similarity index 95% rename from src/hyperspell/types/document.py rename to src/hyperspell/types/memory.py index 69eab7b2..0b9b3fe7 100644 --- a/src/hyperspell/types/document.py +++ b/src/hyperspell/types/memory.py @@ -6,7 +6,7 @@ from .._models import BaseModel -__all__ = ["Document", "Metadata", "MetadataEvent"] +__all__ = ["Memory", "Metadata", "MetadataEvent"] class MetadataEvent(BaseModel): @@ -33,11 +33,12 @@ class Metadata(BaseModel): def __getattr__(self, attr: str) -> object: ... -class Document(BaseModel): +class Memory(BaseModel): resource_id: str source: Literal[ "collections", + "vault", "web_crawler", "notion", "slack", diff --git a/src/hyperspell/types/document_add_params.py b/src/hyperspell/types/memory_add_params.py similarity index 93% rename from src/hyperspell/types/document_add_params.py rename to src/hyperspell/types/memory_add_params.py index 8ebf1534..5c94f1db 100644 --- a/src/hyperspell/types/document_add_params.py +++ b/src/hyperspell/types/memory_add_params.py @@ -8,10 +8,10 @@ from .._utils import PropertyInfo -__all__ = ["DocumentAddParams"] +__all__ = ["MemoryAddParams"] -class DocumentAddParams(TypedDict, total=False): +class MemoryAddParams(TypedDict, total=False): text: Required[str] """Full text of the document.""" diff --git a/src/hyperspell/types/document_list_params.py b/src/hyperspell/types/memory_list_params.py similarity index 93% rename from src/hyperspell/types/document_list_params.py rename to src/hyperspell/types/memory_list_params.py index 53440ca9..c2679fb0 100644 --- a/src/hyperspell/types/document_list_params.py +++ b/src/hyperspell/types/memory_list_params.py @@ -5,10 +5,10 @@ from typing import Optional from typing_extensions import Literal, TypedDict -__all__ = ["DocumentListParams"] +__all__ = ["MemoryListParams"] -class DocumentListParams(TypedDict, total=False): +class MemoryListParams(TypedDict, total=False): collection: Optional[str] """Filter documents by collection.""" @@ -19,6 +19,7 @@ class DocumentListParams(TypedDict, total=False): source: Optional[ Literal[ "collections", + "vault", "web_crawler", "notion", "slack", diff --git a/src/hyperspell/types/query_search_params.py b/src/hyperspell/types/memory_search_params.py similarity index 81% rename from src/hyperspell/types/query_search_params.py rename to src/hyperspell/types/memory_search_params.py index dea983c0..9f97bc11 100644 --- a/src/hyperspell/types/query_search_params.py +++ b/src/hyperspell/types/memory_search_params.py @@ -9,15 +9,17 @@ from .._utils import PropertyInfo __all__ = [ - "QuerySearchParams", + "MemorySearchParams", "Filter", "FilterGoogleCalendar", + "FilterGoogleMail", "FilterNotion", "FilterReddit", "FilterSlack", "FilterWebCrawler", "Options", "OptionsGoogleCalendar", + "OptionsGoogleMail", "OptionsNotion", "OptionsReddit", "OptionsSlack", @@ -25,7 +27,7 @@ ] -class QuerySearchParams(TypedDict, total=False): +class MemorySearchParams(TypedDict, total=False): query: Required[str] """Query to run.""" @@ -47,6 +49,7 @@ class QuerySearchParams(TypedDict, total=False): sources: List[ Literal[ "collections", + "vault", "web_crawler", "notion", "slack", @@ -105,6 +108,16 @@ class FilterGoogleCalendar(TypedDict, total=False): """ +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 FilterNotion(TypedDict, total=False): notion_page_ids: List[str] """List of Notion page IDs to search. @@ -147,6 +160,9 @@ class Filter(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.""" @@ -154,7 +170,7 @@ class Filter(TypedDict, total=False): """Search options for Box""" collections: object - """Search options for Collection""" + """Search options for vault""" google_calendar: FilterGoogleCalendar """Search options for Google Calendar""" @@ -162,6 +178,9 @@ class Filter(TypedDict, total=False): google_drive: object """Search options for Google Drive""" + google_mail: FilterGoogleMail + """Search options for Gmail""" + notion: FilterNotion """Search options for Notion""" @@ -184,6 +203,16 @@ class OptionsGoogleCalendar(TypedDict, total=False): """ +class OptionsGoogleMail(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 OptionsNotion(TypedDict, total=False): notion_page_ids: List[str] """List of Notion page IDs to search. @@ -226,6 +255,9 @@ class Options(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.""" @@ -233,7 +265,7 @@ class Options(TypedDict, total=False): """Search options for Box""" collections: object - """Search options for Collection""" + """Search options for vault""" google_calendar: OptionsGoogleCalendar """Search options for Google Calendar""" @@ -241,6 +273,9 @@ class Options(TypedDict, total=False): google_drive: object """Search options for Google Drive""" + google_mail: OptionsGoogleMail + """Search options for Gmail""" + notion: OptionsNotion """Search options for Notion""" diff --git a/src/hyperspell/types/query_search_response.py b/src/hyperspell/types/memory_search_response.py similarity index 78% rename from src/hyperspell/types/query_search_response.py rename to src/hyperspell/types/memory_search_response.py index e02d76d4..bf6410e8 100644 --- a/src/hyperspell/types/query_search_response.py +++ b/src/hyperspell/types/memory_search_response.py @@ -2,14 +2,14 @@ from typing import Dict, List, Optional +from .memory import Memory from .._models import BaseModel -from .document import Document -__all__ = ["QuerySearchResponse"] +__all__ = ["MemorySearchResponse"] -class QuerySearchResponse(BaseModel): - documents: List[Document] +class MemorySearchResponse(BaseModel): + documents: List[Memory] answer: Optional[str] = None """The answer to the query, if the request was set to answer.""" diff --git a/src/hyperspell/types/document_status.py b/src/hyperspell/types/memory_status.py similarity index 94% rename from src/hyperspell/types/document_status.py rename to src/hyperspell/types/memory_status.py index 7ae05f1a..6d061f77 100644 --- a/src/hyperspell/types/document_status.py +++ b/src/hyperspell/types/memory_status.py @@ -4,10 +4,10 @@ from .._models import BaseModel -__all__ = ["DocumentStatus"] +__all__ = ["MemoryStatus"] -class DocumentStatus(BaseModel): +class MemoryStatus(BaseModel): id: int """Deprecated: refer to documents by source and resource_id instead""" @@ -15,6 +15,7 @@ class DocumentStatus(BaseModel): source: Literal[ "collections", + "vault", "web_crawler", "notion", "slack", diff --git a/src/hyperspell/types/document_status_response.py b/src/hyperspell/types/memory_status_response.py similarity index 73% rename from src/hyperspell/types/document_status_response.py rename to src/hyperspell/types/memory_status_response.py index 022be1c7..7e9bd79b 100644 --- a/src/hyperspell/types/document_status_response.py +++ b/src/hyperspell/types/memory_status_response.py @@ -4,10 +4,10 @@ from .._models import BaseModel -__all__ = ["DocumentStatusResponse"] +__all__ = ["MemoryStatusResponse"] -class DocumentStatusResponse(BaseModel): +class MemoryStatusResponse(BaseModel): providers: Dict[str, Dict[str, int]] total: Dict[str, int] diff --git a/src/hyperspell/types/document_upload_params.py b/src/hyperspell/types/memory_upload_params.py similarity index 81% rename from src/hyperspell/types/document_upload_params.py rename to src/hyperspell/types/memory_upload_params.py index 84300e23..51f93268 100644 --- a/src/hyperspell/types/document_upload_params.py +++ b/src/hyperspell/types/memory_upload_params.py @@ -7,10 +7,10 @@ from .._types import FileTypes -__all__ = ["DocumentUploadParams"] +__all__ = ["MemoryUploadParams"] -class DocumentUploadParams(TypedDict, total=False): +class MemoryUploadParams(TypedDict, total=False): file: Required[FileTypes] """The file to ingest.""" diff --git a/src/hyperspell/types/collection_list_params.py b/src/hyperspell/types/vault_list_params.py similarity index 72% rename from src/hyperspell/types/collection_list_params.py rename to src/hyperspell/types/vault_list_params.py index a6f96181..357671f5 100644 --- a/src/hyperspell/types/collection_list_params.py +++ b/src/hyperspell/types/vault_list_params.py @@ -5,10 +5,10 @@ from typing import Optional from typing_extensions import TypedDict -__all__ = ["CollectionListParams"] +__all__ = ["VaultListParams"] -class CollectionListParams(TypedDict, total=False): +class VaultListParams(TypedDict, total=False): cursor: Optional[str] size: int diff --git a/src/hyperspell/types/collection_list_response.py b/src/hyperspell/types/vault_list_response.py similarity index 73% rename from src/hyperspell/types/collection_list_response.py rename to src/hyperspell/types/vault_list_response.py index 6aebcc93..35237ff5 100644 --- a/src/hyperspell/types/collection_list_response.py +++ b/src/hyperspell/types/vault_list_response.py @@ -4,10 +4,10 @@ from .._models import BaseModel -__all__ = ["CollectionListResponse"] +__all__ = ["VaultListResponse"] -class CollectionListResponse(BaseModel): +class VaultListResponse(BaseModel): collection: Optional[str] = None document_count: int diff --git a/tests/api_resources/integrations/test_google_calendar.py b/tests/api_resources/integrations/test_google_calendar.py index 9aefb1cb..7175efac 100644 --- a/tests/api_resources/integrations/test_google_calendar.py +++ b/tests/api_resources/integrations/test_google_calendar.py @@ -44,7 +44,9 @@ def test_streaming_response_list(self, client: Hyperspell) -> None: class TestAsyncGoogleCalendar: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_list(self, async_client: AsyncHyperspell) -> None: diff --git a/tests/api_resources/integrations/test_web_crawler.py b/tests/api_resources/integrations/test_web_crawler.py index 7a2a2c6a..a40a698b 100644 --- a/tests/api_resources/integrations/test_web_crawler.py +++ b/tests/api_resources/integrations/test_web_crawler.py @@ -50,7 +50,9 @@ def test_streaming_response_index(self, client: Hyperspell) -> None: class TestAsyncWebCrawler: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_index(self, async_client: AsyncHyperspell) -> None: diff --git a/tests/api_resources/test_auth.py b/tests/api_resources/test_auth.py index e4207ba8..f4d88ecb 100644 --- a/tests/api_resources/test_auth.py +++ b/tests/api_resources/test_auth.py @@ -83,7 +83,9 @@ def test_streaming_response_user_token(self, client: Hyperspell) -> None: class TestAsyncAuth: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_me(self, async_client: AsyncHyperspell) -> None: diff --git a/tests/api_resources/test_documents.py b/tests/api_resources/test_documents.py deleted file mode 100644 index 8f85ceba..00000000 --- a/tests/api_resources/test_documents.py +++ /dev/null @@ -1,394 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from hyperspell import Hyperspell, AsyncHyperspell -from tests.utils import assert_matches_type -from hyperspell.types import ( - Document, - DocumentStatus, - DocumentStatusResponse, -) -from hyperspell._utils import parse_datetime -from hyperspell.pagination import SyncCursorPage, AsyncCursorPage - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestDocuments: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @parametrize - def test_method_list(self, client: Hyperspell) -> None: - document = client.documents.list() - assert_matches_type(SyncCursorPage[Document], document, path=["response"]) - - @parametrize - def test_method_list_with_all_params(self, client: Hyperspell) -> None: - document = client.documents.list( - collection="collection", - cursor="cursor", - size=0, - source="collections", - ) - assert_matches_type(SyncCursorPage[Document], document, path=["response"]) - - @parametrize - def test_raw_response_list(self, client: Hyperspell) -> None: - response = client.documents.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = response.parse() - assert_matches_type(SyncCursorPage[Document], document, path=["response"]) - - @parametrize - def test_streaming_response_list(self, client: Hyperspell) -> None: - with client.documents.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = response.parse() - assert_matches_type(SyncCursorPage[Document], document, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - def test_method_add(self, client: Hyperspell) -> None: - document = client.documents.add( - text="text", - ) - assert_matches_type(DocumentStatus, document, path=["response"]) - - @parametrize - def test_method_add_with_all_params(self, client: Hyperspell) -> None: - document = client.documents.add( - text="text", - collection="collection", - date=parse_datetime("2019-12-27T18:11:19.117Z"), - resource_id="resource_id", - title="title", - ) - assert_matches_type(DocumentStatus, document, path=["response"]) - - @parametrize - def test_raw_response_add(self, client: Hyperspell) -> None: - response = client.documents.with_raw_response.add( - text="text", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = response.parse() - assert_matches_type(DocumentStatus, document, path=["response"]) - - @parametrize - def test_streaming_response_add(self, client: Hyperspell) -> None: - with client.documents.with_streaming_response.add( - text="text", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = response.parse() - assert_matches_type(DocumentStatus, document, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - def test_method_get(self, client: Hyperspell) -> None: - document = client.documents.get( - resource_id="resource_id", - source="collections", - ) - assert_matches_type(Document, document, path=["response"]) - - @parametrize - def test_raw_response_get(self, client: Hyperspell) -> None: - response = client.documents.with_raw_response.get( - resource_id="resource_id", - source="collections", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = response.parse() - assert_matches_type(Document, document, path=["response"]) - - @parametrize - def test_streaming_response_get(self, client: Hyperspell) -> None: - with client.documents.with_streaming_response.get( - resource_id="resource_id", - source="collections", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = response.parse() - assert_matches_type(Document, document, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - def test_path_params_get(self, client: Hyperspell) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `resource_id` but received ''"): - client.documents.with_raw_response.get( - resource_id="", - source="collections", - ) - - @parametrize - def test_method_status(self, client: Hyperspell) -> None: - document = client.documents.status() - assert_matches_type(DocumentStatusResponse, document, path=["response"]) - - @parametrize - def test_raw_response_status(self, client: Hyperspell) -> None: - response = client.documents.with_raw_response.status() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = response.parse() - assert_matches_type(DocumentStatusResponse, document, path=["response"]) - - @parametrize - def test_streaming_response_status(self, client: Hyperspell) -> None: - with client.documents.with_streaming_response.status() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = response.parse() - assert_matches_type(DocumentStatusResponse, document, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - def test_method_upload(self, client: Hyperspell) -> None: - document = client.documents.upload( - file=b"raw file contents", - ) - assert_matches_type(DocumentStatus, document, path=["response"]) - - @parametrize - def test_method_upload_with_all_params(self, client: Hyperspell) -> None: - document = client.documents.upload( - file=b"raw file contents", - collection="collection", - ) - assert_matches_type(DocumentStatus, document, path=["response"]) - - @parametrize - def test_raw_response_upload(self, client: Hyperspell) -> None: - response = client.documents.with_raw_response.upload( - file=b"raw file contents", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = response.parse() - assert_matches_type(DocumentStatus, document, path=["response"]) - - @parametrize - def test_streaming_response_upload(self, client: Hyperspell) -> None: - with client.documents.with_streaming_response.upload( - file=b"raw file contents", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = response.parse() - assert_matches_type(DocumentStatus, document, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncDocuments: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) - - @parametrize - async def test_method_list(self, async_client: AsyncHyperspell) -> None: - document = await async_client.documents.list() - assert_matches_type(AsyncCursorPage[Document], document, path=["response"]) - - @parametrize - async def test_method_list_with_all_params(self, async_client: AsyncHyperspell) -> None: - document = await async_client.documents.list( - collection="collection", - cursor="cursor", - size=0, - source="collections", - ) - assert_matches_type(AsyncCursorPage[Document], document, path=["response"]) - - @parametrize - async def test_raw_response_list(self, async_client: AsyncHyperspell) -> None: - response = await async_client.documents.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = await response.parse() - assert_matches_type(AsyncCursorPage[Document], document, path=["response"]) - - @parametrize - async def test_streaming_response_list(self, async_client: AsyncHyperspell) -> None: - async with async_client.documents.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = await response.parse() - assert_matches_type(AsyncCursorPage[Document], document, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - async def test_method_add(self, async_client: AsyncHyperspell) -> None: - document = await async_client.documents.add( - text="text", - ) - assert_matches_type(DocumentStatus, document, path=["response"]) - - @parametrize - async def test_method_add_with_all_params(self, async_client: AsyncHyperspell) -> None: - document = await async_client.documents.add( - text="text", - collection="collection", - date=parse_datetime("2019-12-27T18:11:19.117Z"), - resource_id="resource_id", - title="title", - ) - assert_matches_type(DocumentStatus, document, path=["response"]) - - @parametrize - async def test_raw_response_add(self, async_client: AsyncHyperspell) -> None: - response = await async_client.documents.with_raw_response.add( - text="text", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = await response.parse() - assert_matches_type(DocumentStatus, document, path=["response"]) - - @parametrize - async def test_streaming_response_add(self, async_client: AsyncHyperspell) -> None: - async with async_client.documents.with_streaming_response.add( - text="text", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = await response.parse() - assert_matches_type(DocumentStatus, document, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - async def test_method_get(self, async_client: AsyncHyperspell) -> None: - document = await async_client.documents.get( - resource_id="resource_id", - source="collections", - ) - assert_matches_type(Document, document, path=["response"]) - - @parametrize - async def test_raw_response_get(self, async_client: AsyncHyperspell) -> None: - response = await async_client.documents.with_raw_response.get( - resource_id="resource_id", - source="collections", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = await response.parse() - assert_matches_type(Document, document, path=["response"]) - - @parametrize - async def test_streaming_response_get(self, async_client: AsyncHyperspell) -> None: - async with async_client.documents.with_streaming_response.get( - resource_id="resource_id", - source="collections", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = await response.parse() - assert_matches_type(Document, document, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - async def test_path_params_get(self, async_client: AsyncHyperspell) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `resource_id` but received ''"): - await async_client.documents.with_raw_response.get( - resource_id="", - source="collections", - ) - - @parametrize - async def test_method_status(self, async_client: AsyncHyperspell) -> None: - document = await async_client.documents.status() - assert_matches_type(DocumentStatusResponse, document, path=["response"]) - - @parametrize - async def test_raw_response_status(self, async_client: AsyncHyperspell) -> None: - response = await async_client.documents.with_raw_response.status() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = await response.parse() - assert_matches_type(DocumentStatusResponse, document, path=["response"]) - - @parametrize - async def test_streaming_response_status(self, async_client: AsyncHyperspell) -> None: - async with async_client.documents.with_streaming_response.status() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = await response.parse() - assert_matches_type(DocumentStatusResponse, document, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - async def test_method_upload(self, async_client: AsyncHyperspell) -> None: - document = await async_client.documents.upload( - file=b"raw file contents", - ) - assert_matches_type(DocumentStatus, document, path=["response"]) - - @parametrize - async def test_method_upload_with_all_params(self, async_client: AsyncHyperspell) -> None: - document = await async_client.documents.upload( - file=b"raw file contents", - collection="collection", - ) - assert_matches_type(DocumentStatus, document, path=["response"]) - - @parametrize - async def test_raw_response_upload(self, async_client: AsyncHyperspell) -> None: - response = await async_client.documents.with_raw_response.upload( - file=b"raw file contents", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = await response.parse() - assert_matches_type(DocumentStatus, document, path=["response"]) - - @parametrize - async def test_streaming_response_upload(self, async_client: AsyncHyperspell) -> None: - async with async_client.documents.with_streaming_response.upload( - file=b"raw file contents", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = await response.parse() - assert_matches_type(DocumentStatus, document, path=["response"]) - - assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_integrations.py b/tests/api_resources/test_integrations.py index a2a32b66..25babdd3 100644 --- a/tests/api_resources/test_integrations.py +++ b/tests/api_resources/test_integrations.py @@ -57,7 +57,9 @@ def test_path_params_revoke(self, client: Hyperspell) -> None: class TestAsyncIntegrations: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_revoke(self, async_client: AsyncHyperspell) -> None: diff --git a/tests/api_resources/test_memories.py b/tests/api_resources/test_memories.py new file mode 100644 index 00000000..b08ba4a7 --- /dev/null +++ b/tests/api_resources/test_memories.py @@ -0,0 +1,563 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from hyperspell import Hyperspell, AsyncHyperspell +from tests.utils import assert_matches_type +from hyperspell.types import ( + Memory, + MemoryStatus, + MemorySearchResponse, + MemoryStatusResponse, +) +from hyperspell._utils import parse_datetime +from hyperspell.pagination import SyncCursorPage, AsyncCursorPage + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestMemories: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_list(self, client: Hyperspell) -> None: + memory = client.memories.list() + assert_matches_type(SyncCursorPage[Memory], memory, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Hyperspell) -> None: + memory = client.memories.list( + collection="collection", + cursor="cursor", + size=0, + source="collections", + ) + assert_matches_type(SyncCursorPage[Memory], memory, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Hyperspell) -> None: + response = client.memories.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + memory = response.parse() + assert_matches_type(SyncCursorPage[Memory], memory, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Hyperspell) -> None: + with client.memories.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + memory = response.parse() + assert_matches_type(SyncCursorPage[Memory], memory, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_add(self, client: Hyperspell) -> None: + memory = client.memories.add( + 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", + date=parse_datetime("2019-12-27T18:11:19.117Z"), + resource_id="resource_id", + title="title", + ) + 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", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + memory = response.parse() + assert_matches_type(MemoryStatus, memory, path=["response"]) + + @parametrize + def test_streaming_response_add(self, client: Hyperspell) -> None: + with client.memories.with_streaming_response.add( + text="text", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + memory = response.parse() + assert_matches_type(MemoryStatus, memory, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_get(self, client: Hyperspell) -> None: + memory = client.memories.get( + resource_id="resource_id", + source="collections", + ) + assert_matches_type(Memory, memory, path=["response"]) + + @parametrize + def test_raw_response_get(self, client: Hyperspell) -> None: + response = client.memories.with_raw_response.get( + 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(Memory, memory, path=["response"]) + + @parametrize + def test_streaming_response_get(self, client: Hyperspell) -> None: + with client.memories.with_streaming_response.get( + 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(Memory, memory, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_get(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.get( + resource_id="", + source="collections", + ) + + @parametrize + def test_method_search(self, client: Hyperspell) -> None: + memory = client.memories.search( + query="query", + ) + assert_matches_type(MemorySearchResponse, memory, path=["response"]) + + @parametrize + 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"]}, + "reddit": { + "period": "hour", + "sort": "relevance", + "subreddit": "subreddit", + }, + "slack": {"channels": ["string"]}, + "web_crawler": { + "max_depth": 0, + "url": "string", + }, + }, + sources=["collections"], + ) + assert_matches_type(MemorySearchResponse, memory, path=["response"]) + + @parametrize + def test_raw_response_search(self, client: Hyperspell) -> None: + response = client.memories.with_raw_response.search( + query="query", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + memory = response.parse() + assert_matches_type(MemorySearchResponse, memory, path=["response"]) + + @parametrize + def test_streaming_response_search(self, client: Hyperspell) -> None: + with client.memories.with_streaming_response.search( + query="query", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + memory = response.parse() + assert_matches_type(MemorySearchResponse, memory, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_status(self, client: Hyperspell) -> None: + memory = client.memories.status() + assert_matches_type(MemoryStatusResponse, memory, path=["response"]) + + @parametrize + def test_raw_response_status(self, client: Hyperspell) -> None: + response = client.memories.with_raw_response.status() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + memory = response.parse() + assert_matches_type(MemoryStatusResponse, memory, path=["response"]) + + @parametrize + def test_streaming_response_status(self, client: Hyperspell) -> None: + with client.memories.with_streaming_response.status() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + memory = response.parse() + assert_matches_type(MemoryStatusResponse, memory, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_upload(self, client: Hyperspell) -> None: + memory = client.memories.upload( + file=b"raw file contents", + ) + 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"raw file contents", + collection="collection", + ) + assert_matches_type(MemoryStatus, memory, path=["response"]) + + @parametrize + def test_raw_response_upload(self, client: Hyperspell) -> None: + response = client.memories.with_raw_response.upload( + file=b"raw file contents", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + memory = response.parse() + assert_matches_type(MemoryStatus, memory, path=["response"]) + + @parametrize + def test_streaming_response_upload(self, client: Hyperspell) -> None: + with client.memories.with_streaming_response.upload( + file=b"raw file contents", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + memory = response.parse() + assert_matches_type(MemoryStatus, memory, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncMemories: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_list(self, async_client: AsyncHyperspell) -> None: + memory = await async_client.memories.list() + assert_matches_type(AsyncCursorPage[Memory], memory, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncHyperspell) -> None: + memory = await async_client.memories.list( + collection="collection", + cursor="cursor", + size=0, + source="collections", + ) + assert_matches_type(AsyncCursorPage[Memory], memory, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncHyperspell) -> None: + response = await async_client.memories.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + memory = await response.parse() + assert_matches_type(AsyncCursorPage[Memory], memory, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncHyperspell) -> None: + async with async_client.memories.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + memory = await response.parse() + assert_matches_type(AsyncCursorPage[Memory], memory, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_add(self, async_client: AsyncHyperspell) -> None: + memory = await async_client.memories.add( + 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", + date=parse_datetime("2019-12-27T18:11:19.117Z"), + resource_id="resource_id", + title="title", + ) + 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", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + memory = await response.parse() + assert_matches_type(MemoryStatus, memory, path=["response"]) + + @parametrize + async def test_streaming_response_add(self, async_client: AsyncHyperspell) -> None: + async with async_client.memories.with_streaming_response.add( + text="text", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + memory = await response.parse() + assert_matches_type(MemoryStatus, memory, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_get(self, async_client: AsyncHyperspell) -> None: + memory = await async_client.memories.get( + resource_id="resource_id", + source="collections", + ) + assert_matches_type(Memory, memory, path=["response"]) + + @parametrize + async def test_raw_response_get(self, async_client: AsyncHyperspell) -> None: + response = await async_client.memories.with_raw_response.get( + 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(Memory, memory, path=["response"]) + + @parametrize + async def test_streaming_response_get(self, async_client: AsyncHyperspell) -> None: + async with async_client.memories.with_streaming_response.get( + 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(Memory, memory, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_get(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.get( + resource_id="", + source="collections", + ) + + @parametrize + async def test_method_search(self, async_client: AsyncHyperspell) -> None: + memory = await async_client.memories.search( + query="query", + ) + assert_matches_type(MemorySearchResponse, 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", + 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"]}, + "reddit": { + "period": "hour", + "sort": "relevance", + "subreddit": "subreddit", + }, + "slack": {"channels": ["string"]}, + "web_crawler": { + "max_depth": 0, + "url": "string", + }, + }, + sources=["collections"], + ) + assert_matches_type(MemorySearchResponse, 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", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + memory = await response.parse() + assert_matches_type(MemorySearchResponse, memory, path=["response"]) + + @parametrize + async def test_streaming_response_search(self, async_client: AsyncHyperspell) -> None: + async with async_client.memories.with_streaming_response.search( + query="query", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + memory = await response.parse() + assert_matches_type(MemorySearchResponse, memory, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_status(self, async_client: AsyncHyperspell) -> None: + memory = await async_client.memories.status() + assert_matches_type(MemoryStatusResponse, memory, path=["response"]) + + @parametrize + async def test_raw_response_status(self, async_client: AsyncHyperspell) -> None: + response = await async_client.memories.with_raw_response.status() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + memory = await response.parse() + assert_matches_type(MemoryStatusResponse, memory, path=["response"]) + + @parametrize + async def test_streaming_response_status(self, async_client: AsyncHyperspell) -> None: + async with async_client.memories.with_streaming_response.status() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + memory = await response.parse() + assert_matches_type(MemoryStatusResponse, memory, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_upload(self, async_client: AsyncHyperspell) -> None: + memory = await async_client.memories.upload( + file=b"raw file contents", + ) + 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"raw file contents", + collection="collection", + ) + assert_matches_type(MemoryStatus, memory, path=["response"]) + + @parametrize + async def test_raw_response_upload(self, async_client: AsyncHyperspell) -> None: + response = await async_client.memories.with_raw_response.upload( + file=b"raw file contents", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + memory = await response.parse() + assert_matches_type(MemoryStatus, memory, path=["response"]) + + @parametrize + async def test_streaming_response_upload(self, async_client: AsyncHyperspell) -> None: + async with async_client.memories.with_streaming_response.upload( + file=b"raw file contents", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + memory = await response.parse() + assert_matches_type(MemoryStatus, memory, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_query.py b/tests/api_resources/test_query.py deleted file mode 100644 index 87650761..00000000 --- a/tests/api_resources/test_query.py +++ /dev/null @@ -1,181 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from hyperspell import Hyperspell, AsyncHyperspell -from tests.utils import assert_matches_type -from hyperspell.types import QuerySearchResponse -from hyperspell._utils import parse_datetime - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestQuery: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @parametrize - def test_method_search(self, client: Hyperspell) -> None: - query = client.query.search( - query="query", - ) - assert_matches_type(QuerySearchResponse, query, path=["response"]) - - @parametrize - def test_method_search_with_all_params(self, client: Hyperspell) -> None: - query = client.query.search( - query="query", - answer=True, - filter={ - "after": parse_datetime("2019-12-27T18:11:19.117Z"), - "before": parse_datetime("2019-12-27T18:11:19.117Z"), - "box": {}, - "collections": {}, - "google_calendar": {"calendar_id": "calendar_id"}, - "google_drive": {}, - "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"), - "before": parse_datetime("2019-12-27T18:11:19.117Z"), - "box": {}, - "collections": {}, - "google_calendar": {"calendar_id": "calendar_id"}, - "google_drive": {}, - "notion": {"notion_page_ids": ["string"]}, - "reddit": { - "period": "hour", - "sort": "relevance", - "subreddit": "subreddit", - }, - "slack": {"channels": ["string"]}, - "web_crawler": { - "max_depth": 0, - "url": "string", - }, - }, - sources=["collections"], - ) - assert_matches_type(QuerySearchResponse, query, path=["response"]) - - @parametrize - def test_raw_response_search(self, client: Hyperspell) -> None: - response = client.query.with_raw_response.search( - query="query", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - query = response.parse() - assert_matches_type(QuerySearchResponse, query, path=["response"]) - - @parametrize - def test_streaming_response_search(self, client: Hyperspell) -> None: - with client.query.with_streaming_response.search( - query="query", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - query = response.parse() - assert_matches_type(QuerySearchResponse, query, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncQuery: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) - - @parametrize - async def test_method_search(self, async_client: AsyncHyperspell) -> None: - query = await async_client.query.search( - query="query", - ) - assert_matches_type(QuerySearchResponse, query, path=["response"]) - - @parametrize - async def test_method_search_with_all_params(self, async_client: AsyncHyperspell) -> None: - query = await async_client.query.search( - query="query", - answer=True, - filter={ - "after": parse_datetime("2019-12-27T18:11:19.117Z"), - "before": parse_datetime("2019-12-27T18:11:19.117Z"), - "box": {}, - "collections": {}, - "google_calendar": {"calendar_id": "calendar_id"}, - "google_drive": {}, - "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"), - "before": parse_datetime("2019-12-27T18:11:19.117Z"), - "box": {}, - "collections": {}, - "google_calendar": {"calendar_id": "calendar_id"}, - "google_drive": {}, - "notion": {"notion_page_ids": ["string"]}, - "reddit": { - "period": "hour", - "sort": "relevance", - "subreddit": "subreddit", - }, - "slack": {"channels": ["string"]}, - "web_crawler": { - "max_depth": 0, - "url": "string", - }, - }, - sources=["collections"], - ) - assert_matches_type(QuerySearchResponse, query, path=["response"]) - - @parametrize - async def test_raw_response_search(self, async_client: AsyncHyperspell) -> None: - response = await async_client.query.with_raw_response.search( - query="query", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - query = await response.parse() - assert_matches_type(QuerySearchResponse, query, path=["response"]) - - @parametrize - async def test_streaming_response_search(self, async_client: AsyncHyperspell) -> None: - async with async_client.query.with_streaming_response.search( - query="query", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - query = await response.parse() - assert_matches_type(QuerySearchResponse, query, path=["response"]) - - assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_collections.py b/tests/api_resources/test_vaults.py similarity index 54% rename from tests/api_resources/test_collections.py rename to tests/api_resources/test_vaults.py index 01653ce4..c0e4ab86 100644 --- a/tests/api_resources/test_collections.py +++ b/tests/api_resources/test_vaults.py @@ -9,81 +9,83 @@ from hyperspell import Hyperspell, AsyncHyperspell from tests.utils import assert_matches_type -from hyperspell.types import CollectionListResponse +from hyperspell.types import VaultListResponse from hyperspell.pagination import SyncCursorPage, AsyncCursorPage base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") -class TestCollections: +class TestVaults: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) @parametrize def test_method_list(self, client: Hyperspell) -> None: - collection = client.collections.list() - assert_matches_type(SyncCursorPage[CollectionListResponse], collection, path=["response"]) + vault = client.vaults.list() + assert_matches_type(SyncCursorPage[VaultListResponse], vault, path=["response"]) @parametrize def test_method_list_with_all_params(self, client: Hyperspell) -> None: - collection = client.collections.list( + vault = client.vaults.list( cursor="cursor", size=0, ) - assert_matches_type(SyncCursorPage[CollectionListResponse], collection, path=["response"]) + assert_matches_type(SyncCursorPage[VaultListResponse], vault, path=["response"]) @parametrize def test_raw_response_list(self, client: Hyperspell) -> None: - response = client.collections.with_raw_response.list() + response = client.vaults.with_raw_response.list() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" - collection = response.parse() - assert_matches_type(SyncCursorPage[CollectionListResponse], collection, path=["response"]) + vault = response.parse() + assert_matches_type(SyncCursorPage[VaultListResponse], vault, path=["response"]) @parametrize def test_streaming_response_list(self, client: Hyperspell) -> None: - with client.collections.with_streaming_response.list() as response: + with client.vaults.with_streaming_response.list() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" - collection = response.parse() - assert_matches_type(SyncCursorPage[CollectionListResponse], collection, path=["response"]) + vault = response.parse() + assert_matches_type(SyncCursorPage[VaultListResponse], vault, path=["response"]) assert cast(Any, response.is_closed) is True -class TestAsyncCollections: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) +class TestAsyncVaults: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) @parametrize async def test_method_list(self, async_client: AsyncHyperspell) -> None: - collection = await async_client.collections.list() - assert_matches_type(AsyncCursorPage[CollectionListResponse], collection, path=["response"]) + vault = await async_client.vaults.list() + assert_matches_type(AsyncCursorPage[VaultListResponse], vault, path=["response"]) @parametrize async def test_method_list_with_all_params(self, async_client: AsyncHyperspell) -> None: - collection = await async_client.collections.list( + vault = await async_client.vaults.list( cursor="cursor", size=0, ) - assert_matches_type(AsyncCursorPage[CollectionListResponse], collection, path=["response"]) + assert_matches_type(AsyncCursorPage[VaultListResponse], vault, path=["response"]) @parametrize async def test_raw_response_list(self, async_client: AsyncHyperspell) -> None: - response = await async_client.collections.with_raw_response.list() + response = await async_client.vaults.with_raw_response.list() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" - collection = await response.parse() - assert_matches_type(AsyncCursorPage[CollectionListResponse], collection, path=["response"]) + vault = await response.parse() + assert_matches_type(AsyncCursorPage[VaultListResponse], vault, path=["response"]) @parametrize async def test_streaming_response_list(self, async_client: AsyncHyperspell) -> None: - async with async_client.collections.with_streaming_response.list() as response: + async with async_client.vaults.with_streaming_response.list() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" - collection = await response.parse() - assert_matches_type(AsyncCursorPage[CollectionListResponse], collection, path=["response"]) + vault = await response.parse() + assert_matches_type(AsyncCursorPage[VaultListResponse], vault, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/conftest.py b/tests/conftest.py index 67487fa3..0768c982 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,10 +6,12 @@ import logging from typing import TYPE_CHECKING, Iterator, AsyncIterator +import httpx import pytest from pytest_asyncio import is_async_test -from hyperspell import Hyperspell, AsyncHyperspell +from hyperspell import Hyperspell, AsyncHyperspell, DefaultAioHttpClient +from hyperspell._utils import is_dict if TYPE_CHECKING: from _pytest.fixtures import FixtureRequest # pyright: ignore[reportPrivateImportUsage] @@ -27,6 +29,19 @@ def pytest_collection_modifyitems(items: list[pytest.Function]) -> None: for async_test in pytest_asyncio_tests: async_test.add_marker(session_scope_marker, append=False) + # We skip tests that use both the aiohttp client and respx_mock as respx_mock + # doesn't support custom transports. + for item in items: + if "async_client" not in item.fixturenames or "respx_mock" not in item.fixturenames: + continue + + if not hasattr(item, "callspec"): + continue + + async_client_param = item.callspec.params.get("async_client") + if is_dict(async_client_param) and async_client_param.get("http_client") == "aiohttp": + item.add_marker(pytest.mark.skip(reason="aiohttp client is not compatible with respx_mock")) + base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -46,11 +61,25 @@ def client(request: FixtureRequest) -> Iterator[Hyperspell]: @pytest.fixture(scope="session") async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncHyperspell]: - strict = getattr(request, "param", True) - if not isinstance(strict, bool): - raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") + param = getattr(request, "param", True) + + # defaults + strict = True + http_client: None | httpx.AsyncClient = None + + if isinstance(param, bool): + strict = param + elif is_dict(param): + strict = param.get("strict", True) + assert isinstance(strict, bool) + + http_client_type = param.get("http_client", "httpx") + if http_client_type == "aiohttp": + http_client = DefaultAioHttpClient() + else: + raise TypeError(f"Unexpected fixture parameter type {type(param)}, expected bool or dict") async with AsyncHyperspell( - base_url=base_url, api_key=api_key, user_id=user_id, _strict_response_validation=strict + base_url=base_url, api_key=api_key, user_id=user_id, _strict_response_validation=strict, http_client=http_client ) as client: yield client diff --git a/tests/test_client.py b/tests/test_client.py index 29717c98..8ddca0cf 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -204,6 +204,7 @@ def test_copy_signature(self) -> None: copy_param = copy_signature.parameters.get(name) assert copy_param is not None, f"copy() signature is missing the {name} param" + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") def test_copy_build_request(self) -> None: options = FinalRequestOptions(method="get", url="/foo") @@ -504,7 +505,7 @@ def test_request_extra_query(self) -> None: def test_multipart_repeating_array(self, client: Hyperspell) -> None: request = client._build_request( FinalRequestOptions.construct( - method="get", + method="post", url="/foo", headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, json_data={"array": ["foo", "bar"]}, @@ -782,20 +783,20 @@ def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str @mock.patch("hyperspell._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: Hyperspell) -> None: - respx_mock.post("/documents/add").mock(side_effect=httpx.TimeoutException("Test timeout error")) + respx_mock.post("/memories/add").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - client.documents.with_streaming_response.add(text="text").__enter__() + client.memories.with_streaming_response.add(text="text").__enter__() assert _get_open_connections(self.client) == 0 @mock.patch("hyperspell._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: Hyperspell) -> None: - respx_mock.post("/documents/add").mock(return_value=httpx.Response(500)) + respx_mock.post("/memories/add").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - client.documents.with_streaming_response.add(text="text").__enter__() + client.memories.with_streaming_response.add(text="text").__enter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -822,9 +823,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/documents/add").mock(side_effect=retry_handler) + respx_mock.post("/memories/add").mock(side_effect=retry_handler) - response = client.documents.with_raw_response.add(text="text") + response = client.memories.with_raw_response.add(text="text") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -846,11 +847,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/documents/add").mock(side_effect=retry_handler) + respx_mock.post("/memories/add").mock(side_effect=retry_handler) - response = client.documents.with_raw_response.add( - text="text", extra_headers={"x-stainless-retry-count": Omit()} - ) + response = client.memories.with_raw_response.add(text="text", extra_headers={"x-stainless-retry-count": Omit()}) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -871,9 +870,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/documents/add").mock(side_effect=retry_handler) + respx_mock.post("/memories/add").mock(side_effect=retry_handler) - response = client.documents.with_raw_response.add(text="text", extra_headers={"x-stainless-retry-count": "42"}) + response = client.memories.with_raw_response.add(text="text", extra_headers={"x-stainless-retry-count": "42"}) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1074,6 +1073,7 @@ def test_copy_signature(self) -> None: copy_param = copy_signature.parameters.get(name) assert copy_param is not None, f"copy() signature is missing the {name} param" + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") def test_copy_build_request(self) -> None: options = FinalRequestOptions(method="get", url="/foo") @@ -1376,7 +1376,7 @@ def test_request_extra_query(self) -> None: def test_multipart_repeating_array(self, async_client: AsyncHyperspell) -> None: request = async_client._build_request( FinalRequestOptions.construct( - method="get", + method="post", url="/foo", headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, json_data={"array": ["foo", "bar"]}, @@ -1660,10 +1660,10 @@ async def test_parse_retry_after_header(self, remaining_retries: int, retry_afte async def test_retrying_timeout_errors_doesnt_leak( self, respx_mock: MockRouter, async_client: AsyncHyperspell ) -> None: - respx_mock.post("/documents/add").mock(side_effect=httpx.TimeoutException("Test timeout error")) + respx_mock.post("/memories/add").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await async_client.documents.with_streaming_response.add(text="text").__aenter__() + await async_client.memories.with_streaming_response.add(text="text").__aenter__() assert _get_open_connections(self.client) == 0 @@ -1672,10 +1672,10 @@ async def test_retrying_timeout_errors_doesnt_leak( async def test_retrying_status_errors_doesnt_leak( self, respx_mock: MockRouter, async_client: AsyncHyperspell ) -> None: - respx_mock.post("/documents/add").mock(return_value=httpx.Response(500)) + respx_mock.post("/memories/add").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await async_client.documents.with_streaming_response.add(text="text").__aenter__() + await async_client.memories.with_streaming_response.add(text="text").__aenter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1703,9 +1703,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/documents/add").mock(side_effect=retry_handler) + respx_mock.post("/memories/add").mock(side_effect=retry_handler) - response = await client.documents.with_raw_response.add(text="text") + response = await client.memories.with_raw_response.add(text="text") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1728,9 +1728,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/documents/add").mock(side_effect=retry_handler) + respx_mock.post("/memories/add").mock(side_effect=retry_handler) - response = await client.documents.with_raw_response.add( + response = await client.memories.with_raw_response.add( text="text", extra_headers={"x-stainless-retry-count": Omit()} ) @@ -1754,9 +1754,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/documents/add").mock(side_effect=retry_handler) + respx_mock.post("/memories/add").mock(side_effect=retry_handler) - response = await client.documents.with_raw_response.add( + response = await client.memories.with_raw_response.add( text="text", extra_headers={"x-stainless-retry-count": "42"} ) diff --git a/tests/test_models.py b/tests/test_models.py index b3a94798..cfcb602e 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -889,3 +889,48 @@ class ModelB(BaseModel): ) assert isinstance(m, ModelB) + + +def test_nested_discriminated_union() -> None: + class InnerType1(BaseModel): + type: Literal["type_1"] + + class InnerModel(BaseModel): + inner_value: str + + class InnerType2(BaseModel): + type: Literal["type_2"] + some_inner_model: InnerModel + + class Type1(BaseModel): + base_type: Literal["base_type_1"] + value: Annotated[ + Union[ + InnerType1, + InnerType2, + ], + PropertyInfo(discriminator="type"), + ] + + class Type2(BaseModel): + base_type: Literal["base_type_2"] + + T = Annotated[ + Union[ + Type1, + Type2, + ], + PropertyInfo(discriminator="base_type"), + ] + + model = construct_type( + type_=T, + value={ + "base_type": "base_type_1", + "value": { + "type": "type_2", + }, + }, + ) + assert isinstance(model, Type1) + assert isinstance(model.value, InnerType2)