From 9e862ec83b15db59a377929e7cdba87379f2d6fb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 19:25:32 +0000 Subject: [PATCH 01/23] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index ac4b130c..43ee99d2 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/supermemory--inc%2Fsupermemory-new-ff2201f4ff8f062673cb93068f6d3efeb46d6ac7ce66632418ec1825b03f6332.yml -openapi_spec_hash: 11b52dea5fc829a46baea91d0c7e3c4e +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/supermemory--inc%2Fsupermemory-new-d6de3551059412a664edf9b06252b950508c786b042af5eedae1e989c215b06f.yml +openapi_spec_hash: 1747c680518d17a7e7d988a3c6905388 config_hash: a478b24249ee4f53abfb5787ca4daf8b From 6085dd39d67398eec25d5b9bcc5371f4810622c8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 11 Oct 2025 02:25:52 +0000 Subject: [PATCH 02/23] chore(internal): detect missing future annotations with ruff --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 31501529..7e6c4de2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -224,6 +224,8 @@ select = [ "B", # remove unused imports "F401", + # check for missing future annotations + "FA102", # bare except statements "E722", # unused arguments @@ -246,6 +248,8 @@ unfixable = [ "T203", ] +extend-safe-fixes = ["FA102"] + [tool.ruff.lint.flake8-tidy-imports.banned-api] "functools.lru_cache".msg = "This function does not retain type information for the wrapped function's arguments; The `lru_cache` function from `_utils` should be used instead" From 4d594a5a23092bcb3465008ba51536654d2b4acc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 18 Oct 2025 00:22:01 +0000 Subject: [PATCH 03/23] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 43ee99d2..e1fb37de 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/supermemory--inc%2Fsupermemory-new-d6de3551059412a664edf9b06252b950508c786b042af5eedae1e989c215b06f.yml -openapi_spec_hash: 1747c680518d17a7e7d988a3c6905388 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/supermemory--inc%2Fsupermemory-new-42746c18146d415cd6ddbb389cb291c0b67ed74b95964ae9c55c6d2d28f1b17c.yml +openapi_spec_hash: 21e1b468b84c8a317fcc3d01016b38e6 config_hash: a478b24249ee4f53abfb5787ca4daf8b From 6ad4b613df1d4177fabeaf0b67d984e195af8594 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 18 Oct 2025 02:14:10 +0000 Subject: [PATCH 04/23] chore: bump `httpx-aiohttp` version to 0.1.9 --- pyproject.toml | 2 +- requirements-dev.lock | 2 +- requirements.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7e6c4de2..a88904ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ Homepage = "https://github.com/supermemoryai/python-sdk" Repository = "https://github.com/supermemoryai/python-sdk" [project.optional-dependencies] -aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.8"] +aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"] [tool.rye] managed = true diff --git a/requirements-dev.lock b/requirements-dev.lock index 38e12e51..ffa6c8d8 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -56,7 +56,7 @@ httpx==0.28.1 # via httpx-aiohttp # via respx # via supermemory -httpx-aiohttp==0.1.8 +httpx-aiohttp==0.1.9 # via supermemory idna==3.4 # via anyio diff --git a/requirements.lock b/requirements.lock index de2de686..4563dece 100644 --- a/requirements.lock +++ b/requirements.lock @@ -43,7 +43,7 @@ httpcore==1.0.9 httpx==0.28.1 # via httpx-aiohttp # via supermemory -httpx-aiohttp==0.1.8 +httpx-aiohttp==0.1.9 # via supermemory idna==3.4 # via anyio From d255ce67ed73125d87cec60653ef1ef9a432cfb3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 03:21:59 +0000 Subject: [PATCH 05/23] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index e1fb37de..66c4e27f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/supermemory--inc%2Fsupermemory-new-42746c18146d415cd6ddbb389cb291c0b67ed74b95964ae9c55c6d2d28f1b17c.yml -openapi_spec_hash: 21e1b468b84c8a317fcc3d01016b38e6 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/supermemory--inc%2Fsupermemory-new-16a67abae0e57e099ae0786c0eadfabd081893721b660d50afe2ab0265d52c1a.yml +openapi_spec_hash: f371b0de53a247eeab88d71cc1edb6ca config_hash: a478b24249ee4f53abfb5787ca4daf8b From 40332791a67ddc0985b24bc0d460cf5fb8d39258 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 05:21:58 +0000 Subject: [PATCH 06/23] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 66c4e27f..1e7387c0 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/supermemory--inc%2Fsupermemory-new-16a67abae0e57e099ae0786c0eadfabd081893721b660d50afe2ab0265d52c1a.yml -openapi_spec_hash: f371b0de53a247eeab88d71cc1edb6ca +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/supermemory--inc%2Fsupermemory-new-b3dad40bf7690f4d2179099085eb863299ba5a48f3616ea8275611780b1662a8.yml +openapi_spec_hash: 3971074431cc6ff589ab4ddfbf998df9 config_hash: a478b24249ee4f53abfb5787ca4daf8b From 4e6c8e61653c99c733f862cbb5e08478159d082b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 20:22:04 +0000 Subject: [PATCH 07/23] feat(api): api update --- .stats.yml | 4 +- src/supermemory/resources/search.py | 39 ++--- .../types/search_documents_params.py | 15 +- .../types/search_execute_params.py | 15 +- tests/api_resources/test_search.py | 156 ++++-------------- 5 files changed, 60 insertions(+), 169 deletions(-) diff --git a/.stats.yml b/.stats.yml index 1e7387c0..1943d420 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/supermemory--inc%2Fsupermemory-new-b3dad40bf7690f4d2179099085eb863299ba5a48f3616ea8275611780b1662a8.yml -openapi_spec_hash: 3971074431cc6ff589ab4ddfbf998df9 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/supermemory--inc%2Fsupermemory-new-e61d7828c965d3ed4a4e638705dd71a5572291216a4dd3a9d4f52cbde04d47e7.yml +openapi_spec_hash: 2623fa1606f109694056e589c06eeeb5 config_hash: a478b24249ee4f53abfb5787ca4daf8b diff --git a/src/supermemory/resources/search.py b/src/supermemory/resources/search.py index 15867142..a5eec266 100644 --- a/src/supermemory/resources/search.py +++ b/src/supermemory/resources/search.py @@ -2,9 +2,6 @@ from __future__ import annotations -from typing import List -from typing_extensions import Literal - import httpx from ..types import search_execute_params, search_memories_params, search_documents_params @@ -50,7 +47,7 @@ def documents( self, *, q: str, - categories_filter: List[Literal["technology", "science", "business", "health"]] | Omit = omit, + categories_filter: SequenceNotStr[str] | Omit = omit, chunk_threshold: float | Omit = omit, container_tags: SequenceNotStr[str] | Omit = omit, doc_id: str | Omit = omit, @@ -75,7 +72,7 @@ def documents( Args: q: Search query string - categories_filter: Optional category filters + categories_filter: DEPRECATED: Optional category filters chunk_threshold: Threshold / sensitivity for chunk selection. 0 is least sensitive (returns most chunks, more results), 1 is most sensitive (returns lesser chunks, accurate @@ -87,9 +84,8 @@ def documents( doc_id: Optional document ID to search within. You can use this to find chunks in a very large document. - document_threshold: Threshold / sensitivity for document selection. 0 is least sensitive (returns - most documents, more results), 1 is most sensitive (returns lesser documents, - accurate results) + document_threshold: DEPRECATED: This field is no longer used in v3 search. The search now uses + chunkThreshold only. This parameter will be ignored. filters: Optional filters to apply to the search. Can be a JSON string or Query object. @@ -149,7 +145,7 @@ def execute( self, *, q: str, - categories_filter: List[Literal["technology", "science", "business", "health"]] | Omit = omit, + categories_filter: SequenceNotStr[str] | Omit = omit, chunk_threshold: float | Omit = omit, container_tags: SequenceNotStr[str] | Omit = omit, doc_id: str | Omit = omit, @@ -174,7 +170,7 @@ def execute( Args: q: Search query string - categories_filter: Optional category filters + categories_filter: DEPRECATED: Optional category filters chunk_threshold: Threshold / sensitivity for chunk selection. 0 is least sensitive (returns most chunks, more results), 1 is most sensitive (returns lesser chunks, accurate @@ -186,9 +182,8 @@ def execute( doc_id: Optional document ID to search within. You can use this to find chunks in a very large document. - document_threshold: Threshold / sensitivity for document selection. 0 is least sensitive (returns - most documents, more results), 1 is most sensitive (returns lesser documents, - accurate results) + document_threshold: DEPRECATED: This field is no longer used in v3 search. The search now uses + chunkThreshold only. This parameter will be ignored. filters: Optional filters to apply to the search. Can be a JSON string or Query object. @@ -339,7 +334,7 @@ async def documents( self, *, q: str, - categories_filter: List[Literal["technology", "science", "business", "health"]] | Omit = omit, + categories_filter: SequenceNotStr[str] | Omit = omit, chunk_threshold: float | Omit = omit, container_tags: SequenceNotStr[str] | Omit = omit, doc_id: str | Omit = omit, @@ -364,7 +359,7 @@ async def documents( Args: q: Search query string - categories_filter: Optional category filters + categories_filter: DEPRECATED: Optional category filters chunk_threshold: Threshold / sensitivity for chunk selection. 0 is least sensitive (returns most chunks, more results), 1 is most sensitive (returns lesser chunks, accurate @@ -376,9 +371,8 @@ async def documents( doc_id: Optional document ID to search within. You can use this to find chunks in a very large document. - document_threshold: Threshold / sensitivity for document selection. 0 is least sensitive (returns - most documents, more results), 1 is most sensitive (returns lesser documents, - accurate results) + document_threshold: DEPRECATED: This field is no longer used in v3 search. The search now uses + chunkThreshold only. This parameter will be ignored. filters: Optional filters to apply to the search. Can be a JSON string or Query object. @@ -438,7 +432,7 @@ async def execute( self, *, q: str, - categories_filter: List[Literal["technology", "science", "business", "health"]] | Omit = omit, + categories_filter: SequenceNotStr[str] | Omit = omit, chunk_threshold: float | Omit = omit, container_tags: SequenceNotStr[str] | Omit = omit, doc_id: str | Omit = omit, @@ -463,7 +457,7 @@ async def execute( Args: q: Search query string - categories_filter: Optional category filters + categories_filter: DEPRECATED: Optional category filters chunk_threshold: Threshold / sensitivity for chunk selection. 0 is least sensitive (returns most chunks, more results), 1 is most sensitive (returns lesser chunks, accurate @@ -475,9 +469,8 @@ async def execute( doc_id: Optional document ID to search within. You can use this to find chunks in a very large document. - document_threshold: Threshold / sensitivity for document selection. 0 is least sensitive (returns - most documents, more results), 1 is most sensitive (returns lesser documents, - accurate results) + document_threshold: DEPRECATED: This field is no longer used in v3 search. The search now uses + chunkThreshold only. This parameter will be ignored. filters: Optional filters to apply to the search. Can be a JSON string or Query object. diff --git a/src/supermemory/types/search_documents_params.py b/src/supermemory/types/search_documents_params.py index 3d397423..20f62b3c 100644 --- a/src/supermemory/types/search_documents_params.py +++ b/src/supermemory/types/search_documents_params.py @@ -2,8 +2,8 @@ from __future__ import annotations -from typing import List, Union -from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict +from typing import Union +from typing_extensions import Required, Annotated, TypeAlias, TypedDict from .._types import SequenceNotStr from .._utils import PropertyInfo @@ -17,10 +17,8 @@ class SearchDocumentsParams(TypedDict, total=False): q: Required[str] """Search query string""" - categories_filter: Annotated[ - List[Literal["technology", "science", "business", "health"]], PropertyInfo(alias="categoriesFilter") - ] - """Optional category filters""" + categories_filter: Annotated[SequenceNotStr[str], PropertyInfo(alias="categoriesFilter")] + """DEPRECATED: Optional category filters""" chunk_threshold: Annotated[float, PropertyInfo(alias="chunkThreshold")] """Threshold / sensitivity for chunk selection. @@ -43,10 +41,9 @@ class SearchDocumentsParams(TypedDict, total=False): """ document_threshold: Annotated[float, PropertyInfo(alias="documentThreshold")] - """Threshold / sensitivity for document selection. + """DEPRECATED: This field is no longer used in v3 search. - 0 is least sensitive (returns most documents, more results), 1 is most sensitive - (returns lesser documents, accurate results) + The search now uses chunkThreshold only. This parameter will be ignored. """ filters: Filters diff --git a/src/supermemory/types/search_execute_params.py b/src/supermemory/types/search_execute_params.py index c4fe69b8..da786065 100644 --- a/src/supermemory/types/search_execute_params.py +++ b/src/supermemory/types/search_execute_params.py @@ -2,8 +2,8 @@ from __future__ import annotations -from typing import List, Union -from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict +from typing import Union +from typing_extensions import Required, Annotated, TypeAlias, TypedDict from .._types import SequenceNotStr from .._utils import PropertyInfo @@ -17,10 +17,8 @@ class SearchExecuteParams(TypedDict, total=False): q: Required[str] """Search query string""" - categories_filter: Annotated[ - List[Literal["technology", "science", "business", "health"]], PropertyInfo(alias="categoriesFilter") - ] - """Optional category filters""" + categories_filter: Annotated[SequenceNotStr[str], PropertyInfo(alias="categoriesFilter")] + """DEPRECATED: Optional category filters""" chunk_threshold: Annotated[float, PropertyInfo(alias="chunkThreshold")] """Threshold / sensitivity for chunk selection. @@ -43,10 +41,9 @@ class SearchExecuteParams(TypedDict, total=False): """ document_threshold: Annotated[float, PropertyInfo(alias="documentThreshold")] - """Threshold / sensitivity for document selection. + """DEPRECATED: This field is no longer used in v3 search. - 0 is least sensitive (returns most documents, more results), 1 is most sensitive - (returns lesser documents, accurate results) + The search now uses chunkThreshold only. This parameter will be ignored. """ filters: Filters diff --git a/tests/api_resources/test_search.py b/tests/api_resources/test_search.py index 49e06305..0cdb87c7 100644 --- a/tests/api_resources/test_search.py +++ b/tests/api_resources/test_search.py @@ -34,32 +34,16 @@ def test_method_documents(self, client: Supermemory) -> None: def test_method_documents_with_all_params(self, client: Supermemory) -> None: search = client.search.documents( q="machine learning concepts", - categories_filter=["technology", "science"], + categories_filter=["string"], chunk_threshold=0.5, - container_tags=["user_123", "project_123"], - doc_id="doc_xyz789", - document_threshold=0.5, - filters={ - "and_": [ - { - "filterType": "metadata", - "key": "group", - "negate": False, - "value": "jira_users", - }, - { - "filterType": "numeric", - "key": "timestamp", - "negate": False, - "numericOperator": ">", - "value": "1742745777", - }, - ] - }, + container_tags=["user_123"], + doc_id="docId", + document_threshold=0, + filters={"or_": [{}]}, include_full_docs=False, - include_summary=False, + include_summary=True, limit=10, - only_matching_chunks=False, + only_matching_chunks=True, rerank=False, rewrite_query=False, ) @@ -104,32 +88,16 @@ def test_method_execute(self, client: Supermemory) -> None: def test_method_execute_with_all_params(self, client: Supermemory) -> None: search = client.search.execute( q="machine learning concepts", - categories_filter=["technology", "science"], + categories_filter=["string"], chunk_threshold=0.5, - container_tags=["user_123", "project_123"], - doc_id="doc_xyz789", - document_threshold=0.5, - filters={ - "and_": [ - { - "filterType": "metadata", - "key": "group", - "negate": False, - "value": "jira_users", - }, - { - "filterType": "numeric", - "key": "timestamp", - "negate": False, - "numericOperator": ">", - "value": "1742745777", - }, - ] - }, + container_tags=["user_123"], + doc_id="docId", + document_threshold=0, + filters={"or_": [{}]}, include_full_docs=False, - include_summary=False, + include_summary=True, limit=10, - only_matching_chunks=False, + only_matching_chunks=True, rerank=False, rewrite_query=False, ) @@ -175,23 +143,7 @@ def test_method_memories_with_all_params(self, client: Supermemory) -> None: search = client.search.memories( q="machine learning concepts", container_tag="user_123", - filters={ - "and_": [ - { - "filterType": "metadata", - "key": "group", - "negate": False, - "value": "jira_users", - }, - { - "filterType": "numeric", - "key": "timestamp", - "negate": False, - "numericOperator": ">", - "value": "1742745777", - }, - ] - }, + filters={"or_": [{}]}, include={ "documents": True, "forgotten_memories": False, @@ -250,32 +202,16 @@ async def test_method_documents(self, async_client: AsyncSupermemory) -> None: async def test_method_documents_with_all_params(self, async_client: AsyncSupermemory) -> None: search = await async_client.search.documents( q="machine learning concepts", - categories_filter=["technology", "science"], + categories_filter=["string"], chunk_threshold=0.5, - container_tags=["user_123", "project_123"], - doc_id="doc_xyz789", - document_threshold=0.5, - filters={ - "and_": [ - { - "filterType": "metadata", - "key": "group", - "negate": False, - "value": "jira_users", - }, - { - "filterType": "numeric", - "key": "timestamp", - "negate": False, - "numericOperator": ">", - "value": "1742745777", - }, - ] - }, + container_tags=["user_123"], + doc_id="docId", + document_threshold=0, + filters={"or_": [{}]}, include_full_docs=False, - include_summary=False, + include_summary=True, limit=10, - only_matching_chunks=False, + only_matching_chunks=True, rerank=False, rewrite_query=False, ) @@ -320,32 +256,16 @@ async def test_method_execute(self, async_client: AsyncSupermemory) -> None: async def test_method_execute_with_all_params(self, async_client: AsyncSupermemory) -> None: search = await async_client.search.execute( q="machine learning concepts", - categories_filter=["technology", "science"], + categories_filter=["string"], chunk_threshold=0.5, - container_tags=["user_123", "project_123"], - doc_id="doc_xyz789", - document_threshold=0.5, - filters={ - "and_": [ - { - "filterType": "metadata", - "key": "group", - "negate": False, - "value": "jira_users", - }, - { - "filterType": "numeric", - "key": "timestamp", - "negate": False, - "numericOperator": ">", - "value": "1742745777", - }, - ] - }, + container_tags=["user_123"], + doc_id="docId", + document_threshold=0, + filters={"or_": [{}]}, include_full_docs=False, - include_summary=False, + include_summary=True, limit=10, - only_matching_chunks=False, + only_matching_chunks=True, rerank=False, rewrite_query=False, ) @@ -391,23 +311,7 @@ async def test_method_memories_with_all_params(self, async_client: AsyncSupermem search = await async_client.search.memories( q="machine learning concepts", container_tag="user_123", - filters={ - "and_": [ - { - "filterType": "metadata", - "key": "group", - "negate": False, - "value": "jira_users", - }, - { - "filterType": "numeric", - "key": "timestamp", - "negate": False, - "numericOperator": ">", - "value": "1742745777", - }, - ] - }, + filters={"or_": [{}]}, include={ "documents": True, "forgotten_memories": False, From 1155c4396baa8681dcb72f6b53f90c42052fac6f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 02:25:08 +0000 Subject: [PATCH 08/23] fix(client): close streams without requiring full consumption --- src/supermemory/_streaming.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/supermemory/_streaming.py b/src/supermemory/_streaming.py index 0b32befb..5e12f66d 100644 --- a/src/supermemory/_streaming.py +++ b/src/supermemory/_streaming.py @@ -57,9 +57,8 @@ def __stream__(self) -> Iterator[_T]: for sse in iterator: yield process_data(data=sse.json(), cast_to=cast_to, response=response) - # Ensure the entire stream is consumed - for _sse in iterator: - ... + # As we might not fully consume the response stream, we need to close it explicitly + response.close() def __enter__(self) -> Self: return self @@ -121,9 +120,8 @@ async def __stream__(self) -> AsyncIterator[_T]: async for sse in iterator: yield process_data(data=sse.json(), cast_to=cast_to, response=response) - # Ensure the entire stream is consumed - async for _sse in iterator: - ... + # As we might not fully consume the response stream, we need to close it explicitly + await response.aclose() async def __aenter__(self) -> Self: return self From a776c2cfcf82ee239c707b9065bd3313755388fa Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 16:22:02 +0000 Subject: [PATCH 09/23] feat(api): api update --- .stats.yml | 4 +- README.md | 10 ++-- src/supermemory/resources/documents.py | 50 ++++--------------- src/supermemory/resources/memories.py | 50 ++++--------------- src/supermemory/types/document_add_params.py | 25 ++-------- .../types/document_add_response.py | 2 + .../types/document_update_response.py | 2 + .../types/document_upload_file_response.py | 2 + src/supermemory/types/memory_add_params.py | 25 ++-------- src/supermemory/types/memory_add_response.py | 2 + .../types/memory_update_response.py | 2 + .../types/memory_upload_file_response.py | 2 + tests/api_resources/test_documents.py | 46 ++++++----------- tests/api_resources/test_memories.py | 46 ++++++----------- tests/test_client.py | 36 ++++--------- 15 files changed, 87 insertions(+), 217 deletions(-) diff --git a/.stats.yml b/.stats.yml index 1943d420..b0d4915a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/supermemory--inc%2Fsupermemory-new-e61d7828c965d3ed4a4e638705dd71a5572291216a4dd3a9d4f52cbde04d47e7.yml -openapi_spec_hash: 2623fa1606f109694056e589c06eeeb5 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/supermemory--inc%2Fsupermemory-new-4565eada92d3d7a0434e5896c283f460888b24f3ed5389f32bcbc2b104fb52df.yml +openapi_spec_hash: 124bf13a8b2727edb1b10dd2274cc2d2 config_hash: a478b24249ee4f53abfb5787ca4daf8b diff --git a/README.md b/README.md index 113af8d0..b49bc5f7 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ client = Supermemory() try: client.memories.add( - content="This is a detailed article about machine learning concepts...", + content="content", ) except supermemory.APIConnectionError as e: print("The server could not be reached") @@ -206,7 +206,7 @@ client = Supermemory( # Or, configure per-request: client.with_options(max_retries=5).memories.add( - content="This is a detailed article about machine learning concepts...", + content="content", ) ``` @@ -231,7 +231,7 @@ client = Supermemory( # Override per-request: client.with_options(timeout=5.0).memories.add( - content="This is a detailed article about machine learning concepts...", + content="content", ) ``` @@ -274,7 +274,7 @@ from supermemory import Supermemory client = Supermemory() response = client.memories.with_raw_response.add( - content="This is a detailed article about machine learning concepts...", + content="content", ) print(response.headers.get('X-My-Header')) @@ -294,7 +294,7 @@ To stream the response body, use `.with_streaming_response` instead, which requi ```python with client.memories.with_streaming_response.add( - content="This is a detailed article about machine learning concepts...", + content="content", ) as response: print(response.headers.get("X-My-Header")) diff --git a/src/supermemory/resources/documents.py b/src/supermemory/resources/documents.py index d1623b02..638be972 100644 --- a/src/supermemory/resources/documents.py +++ b/src/supermemory/resources/documents.py @@ -244,28 +244,13 @@ def add( content: The content to extract and process into a document. This can be a URL to a website, a PDF, an image, or a video. - Plaintext: Any plaintext format - - URL: A URL to a website, PDF, image, or video - - We automatically detect the content type from the url's response format. - - container_tag: Optional tag this document should be containerized by. This can be an ID for - your user, a project ID, or any other identifier you wish to use to group - documents. - - container_tags: (DEPRECATED: Use containerTag instead) Optional tags this document should be - containerized by. This can be an ID for your user, a project ID, or any other - identifier you wish to use to group documents. + container_tag: Optional tag this document should be containerized by. Max 100 characters, + alphanumeric with hyphens and underscores only. - custom_id: Optional custom ID of the document. This could be an ID from your database that - will uniquely identify this document. + custom_id: Optional custom ID of the document. Max 100 characters, alphanumeric with + hyphens and underscores only. - metadata: Optional metadata for the document. This is used to store additional information - about the document. You can use this to store any additional information you - need about the document. Metadata can be filtered through. Keys must be strings - and are case sensitive. Values can be strings, numbers, or booleans. You cannot - nest objects. + metadata: Optional metadata for the document. extra_headers: Send extra headers @@ -612,28 +597,13 @@ async def add( content: The content to extract and process into a document. This can be a URL to a website, a PDF, an image, or a video. - Plaintext: Any plaintext format - - URL: A URL to a website, PDF, image, or video - - We automatically detect the content type from the url's response format. - - container_tag: Optional tag this document should be containerized by. This can be an ID for - your user, a project ID, or any other identifier you wish to use to group - documents. - - container_tags: (DEPRECATED: Use containerTag instead) Optional tags this document should be - containerized by. This can be an ID for your user, a project ID, or any other - identifier you wish to use to group documents. + container_tag: Optional tag this document should be containerized by. Max 100 characters, + alphanumeric with hyphens and underscores only. - custom_id: Optional custom ID of the document. This could be an ID from your database that - will uniquely identify this document. + custom_id: Optional custom ID of the document. Max 100 characters, alphanumeric with + hyphens and underscores only. - metadata: Optional metadata for the document. This is used to store additional information - about the document. You can use this to store any additional information you - need about the document. Metadata can be filtered through. Keys must be strings - and are case sensitive. Values can be strings, numbers, or booleans. You cannot - nest objects. + metadata: Optional metadata for the document. extra_headers: Send extra headers diff --git a/src/supermemory/resources/memories.py b/src/supermemory/resources/memories.py index d025223a..6a96e255 100644 --- a/src/supermemory/resources/memories.py +++ b/src/supermemory/resources/memories.py @@ -244,28 +244,13 @@ def add( content: The content to extract and process into a document. This can be a URL to a website, a PDF, an image, or a video. - Plaintext: Any plaintext format - - URL: A URL to a website, PDF, image, or video - - We automatically detect the content type from the url's response format. - - container_tag: Optional tag this document should be containerized by. This can be an ID for - your user, a project ID, or any other identifier you wish to use to group - documents. - - container_tags: (DEPRECATED: Use containerTag instead) Optional tags this document should be - containerized by. This can be an ID for your user, a project ID, or any other - identifier you wish to use to group documents. + container_tag: Optional tag this document should be containerized by. Max 100 characters, + alphanumeric with hyphens and underscores only. - custom_id: Optional custom ID of the document. This could be an ID from your database that - will uniquely identify this document. + custom_id: Optional custom ID of the document. Max 100 characters, alphanumeric with + hyphens and underscores only. - metadata: Optional metadata for the document. This is used to store additional information - about the document. You can use this to store any additional information you - need about the document. Metadata can be filtered through. Keys must be strings - and are case sensitive. Values can be strings, numbers, or booleans. You cannot - nest objects. + metadata: Optional metadata for the document. extra_headers: Send extra headers @@ -612,28 +597,13 @@ async def add( content: The content to extract and process into a document. This can be a URL to a website, a PDF, an image, or a video. - Plaintext: Any plaintext format - - URL: A URL to a website, PDF, image, or video - - We automatically detect the content type from the url's response format. - - container_tag: Optional tag this document should be containerized by. This can be an ID for - your user, a project ID, or any other identifier you wish to use to group - documents. - - container_tags: (DEPRECATED: Use containerTag instead) Optional tags this document should be - containerized by. This can be an ID for your user, a project ID, or any other - identifier you wish to use to group documents. + container_tag: Optional tag this document should be containerized by. Max 100 characters, + alphanumeric with hyphens and underscores only. - custom_id: Optional custom ID of the document. This could be an ID from your database that - will uniquely identify this document. + custom_id: Optional custom ID of the document. Max 100 characters, alphanumeric with + hyphens and underscores only. - metadata: Optional metadata for the document. This is used to store additional information - about the document. You can use this to store any additional information you - need about the document. Metadata can be filtered through. Keys must be strings - and are case sensitive. Values can be strings, numbers, or booleans. You cannot - nest objects. + metadata: Optional metadata for the document. extra_headers: Send extra headers diff --git a/src/supermemory/types/document_add_params.py b/src/supermemory/types/document_add_params.py index 81a977f6..26c28308 100644 --- a/src/supermemory/types/document_add_params.py +++ b/src/supermemory/types/document_add_params.py @@ -16,40 +16,21 @@ class DocumentAddParams(TypedDict, total=False): """The content to extract and process into a document. This can be a URL to a website, a PDF, an image, or a video. - - Plaintext: Any plaintext format - - URL: A URL to a website, PDF, image, or video - - We automatically detect the content type from the url's response format. """ container_tag: Annotated[str, PropertyInfo(alias="containerTag")] """Optional tag this document should be containerized by. - This can be an ID for your user, a project ID, or any other identifier you wish - to use to group documents. + Max 100 characters, alphanumeric with hyphens and underscores only. """ container_tags: Annotated[SequenceNotStr[str], PropertyInfo(alias="containerTags")] - """ - (DEPRECATED: Use containerTag instead) Optional tags this document should be - containerized by. This can be an ID for your user, a project ID, or any other - identifier you wish to use to group documents. - """ custom_id: Annotated[str, PropertyInfo(alias="customId")] """Optional custom ID of the document. - This could be an ID from your database that will uniquely identify this - document. + Max 100 characters, alphanumeric with hyphens and underscores only. """ metadata: Dict[str, Union[str, float, bool, SequenceNotStr[str]]] - """Optional metadata for the document. - - This is used to store additional information about the document. You can use - this to store any additional information you need about the document. Metadata - can be filtered through. Keys must be strings and are case sensitive. Values can - be strings, numbers, or booleans. You cannot nest objects. - """ + """Optional metadata for the document.""" diff --git a/src/supermemory/types/document_add_response.py b/src/supermemory/types/document_add_response.py index 5a123794..e6a5f01d 100644 --- a/src/supermemory/types/document_add_response.py +++ b/src/supermemory/types/document_add_response.py @@ -7,5 +7,7 @@ class DocumentAddResponse(BaseModel): id: str + """Unique identifier of the document""" status: str + """Status of the document""" diff --git a/src/supermemory/types/document_update_response.py b/src/supermemory/types/document_update_response.py index 3b27289e..464d9c45 100644 --- a/src/supermemory/types/document_update_response.py +++ b/src/supermemory/types/document_update_response.py @@ -7,5 +7,7 @@ class DocumentUpdateResponse(BaseModel): id: str + """Unique identifier of the document""" status: str + """Status of the document""" diff --git a/src/supermemory/types/document_upload_file_response.py b/src/supermemory/types/document_upload_file_response.py index 5801bcab..218a07d4 100644 --- a/src/supermemory/types/document_upload_file_response.py +++ b/src/supermemory/types/document_upload_file_response.py @@ -7,5 +7,7 @@ class DocumentUploadFileResponse(BaseModel): id: str + """Unique identifier of the document""" status: str + """Status of the document""" diff --git a/src/supermemory/types/memory_add_params.py b/src/supermemory/types/memory_add_params.py index 0ae2e78f..73aa26ce 100644 --- a/src/supermemory/types/memory_add_params.py +++ b/src/supermemory/types/memory_add_params.py @@ -16,40 +16,21 @@ class MemoryAddParams(TypedDict, total=False): """The content to extract and process into a document. This can be a URL to a website, a PDF, an image, or a video. - - Plaintext: Any plaintext format - - URL: A URL to a website, PDF, image, or video - - We automatically detect the content type from the url's response format. """ container_tag: Annotated[str, PropertyInfo(alias="containerTag")] """Optional tag this document should be containerized by. - This can be an ID for your user, a project ID, or any other identifier you wish - to use to group documents. + Max 100 characters, alphanumeric with hyphens and underscores only. """ container_tags: Annotated[SequenceNotStr[str], PropertyInfo(alias="containerTags")] - """ - (DEPRECATED: Use containerTag instead) Optional tags this document should be - containerized by. This can be an ID for your user, a project ID, or any other - identifier you wish to use to group documents. - """ custom_id: Annotated[str, PropertyInfo(alias="customId")] """Optional custom ID of the document. - This could be an ID from your database that will uniquely identify this - document. + Max 100 characters, alphanumeric with hyphens and underscores only. """ metadata: Dict[str, Union[str, float, bool, SequenceNotStr[str]]] - """Optional metadata for the document. - - This is used to store additional information about the document. You can use - this to store any additional information you need about the document. Metadata - can be filtered through. Keys must be strings and are case sensitive. Values can - be strings, numbers, or booleans. You cannot nest objects. - """ + """Optional metadata for the document.""" diff --git a/src/supermemory/types/memory_add_response.py b/src/supermemory/types/memory_add_response.py index 704918e4..6a1d7f3e 100644 --- a/src/supermemory/types/memory_add_response.py +++ b/src/supermemory/types/memory_add_response.py @@ -7,5 +7,7 @@ class MemoryAddResponse(BaseModel): id: str + """Unique identifier of the document""" status: str + """Status of the document""" diff --git a/src/supermemory/types/memory_update_response.py b/src/supermemory/types/memory_update_response.py index 132b8cf9..e2e6ed4e 100644 --- a/src/supermemory/types/memory_update_response.py +++ b/src/supermemory/types/memory_update_response.py @@ -7,5 +7,7 @@ class MemoryUpdateResponse(BaseModel): id: str + """Unique identifier of the document""" status: str + """Status of the document""" diff --git a/src/supermemory/types/memory_upload_file_response.py b/src/supermemory/types/memory_upload_file_response.py index f67b958f..0f2e6ed7 100644 --- a/src/supermemory/types/memory_upload_file_response.py +++ b/src/supermemory/types/memory_upload_file_response.py @@ -7,5 +7,7 @@ class MemoryUploadFileResponse(BaseModel): id: str + """Unique identifier of the document""" status: str + """Status of the document""" diff --git a/tests/api_resources/test_documents.py b/tests/api_resources/test_documents.py index ffddf224..7aecc421 100644 --- a/tests/api_resources/test_documents.py +++ b/tests/api_resources/test_documents.py @@ -189,7 +189,7 @@ def test_path_params_delete(self, client: Supermemory) -> None: @parametrize def test_method_add(self, client: Supermemory) -> None: document = client.documents.add( - content="This is a detailed article about machine learning concepts...", + content="content", ) assert_matches_type(DocumentAddResponse, document, path=["response"]) @@ -197,18 +197,11 @@ def test_method_add(self, client: Supermemory) -> None: @parametrize def test_method_add_with_all_params(self, client: Supermemory) -> None: document = client.documents.add( - content="This is a detailed article about machine learning concepts...", - container_tag="user_123", - container_tags=["user_123", "project_123"], - custom_id="mem_abc123", - metadata={ - "category": "technology", - "isPublic": True, - "readingTime": 5, - "source": "web", - "tag_1": "ai", - "tag_2": "machine-learning", - }, + content="content", + container_tag="containerTag", + container_tags=["string"], + custom_id="customId", + metadata={"foo": "string"}, ) assert_matches_type(DocumentAddResponse, document, path=["response"]) @@ -216,7 +209,7 @@ def test_method_add_with_all_params(self, client: Supermemory) -> None: @parametrize def test_raw_response_add(self, client: Supermemory) -> None: response = client.documents.with_raw_response.add( - content="This is a detailed article about machine learning concepts...", + content="content", ) assert response.is_closed is True @@ -228,7 +221,7 @@ def test_raw_response_add(self, client: Supermemory) -> None: @parametrize def test_streaming_response_add(self, client: Supermemory) -> None: with client.documents.with_streaming_response.add( - content="This is a detailed article about machine learning concepts...", + content="content", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -498,7 +491,7 @@ async def test_path_params_delete(self, async_client: AsyncSupermemory) -> None: @parametrize async def test_method_add(self, async_client: AsyncSupermemory) -> None: document = await async_client.documents.add( - content="This is a detailed article about machine learning concepts...", + content="content", ) assert_matches_type(DocumentAddResponse, document, path=["response"]) @@ -506,18 +499,11 @@ async def test_method_add(self, async_client: AsyncSupermemory) -> None: @parametrize async def test_method_add_with_all_params(self, async_client: AsyncSupermemory) -> None: document = await async_client.documents.add( - content="This is a detailed article about machine learning concepts...", - container_tag="user_123", - container_tags=["user_123", "project_123"], - custom_id="mem_abc123", - metadata={ - "category": "technology", - "isPublic": True, - "readingTime": 5, - "source": "web", - "tag_1": "ai", - "tag_2": "machine-learning", - }, + content="content", + container_tag="containerTag", + container_tags=["string"], + custom_id="customId", + metadata={"foo": "string"}, ) assert_matches_type(DocumentAddResponse, document, path=["response"]) @@ -525,7 +511,7 @@ async def test_method_add_with_all_params(self, async_client: AsyncSupermemory) @parametrize async def test_raw_response_add(self, async_client: AsyncSupermemory) -> None: response = await async_client.documents.with_raw_response.add( - content="This is a detailed article about machine learning concepts...", + content="content", ) assert response.is_closed is True @@ -537,7 +523,7 @@ async def test_raw_response_add(self, async_client: AsyncSupermemory) -> None: @parametrize async def test_streaming_response_add(self, async_client: AsyncSupermemory) -> None: async with async_client.documents.with_streaming_response.add( - content="This is a detailed article about machine learning concepts...", + content="content", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/api_resources/test_memories.py b/tests/api_resources/test_memories.py index 1018413e..3ac5c35d 100644 --- a/tests/api_resources/test_memories.py +++ b/tests/api_resources/test_memories.py @@ -189,7 +189,7 @@ def test_path_params_delete(self, client: Supermemory) -> None: @parametrize def test_method_add(self, client: Supermemory) -> None: memory = client.memories.add( - content="This is a detailed article about machine learning concepts...", + content="content", ) assert_matches_type(MemoryAddResponse, memory, path=["response"]) @@ -197,18 +197,11 @@ def test_method_add(self, client: Supermemory) -> None: @parametrize def test_method_add_with_all_params(self, client: Supermemory) -> None: memory = client.memories.add( - content="This is a detailed article about machine learning concepts...", - container_tag="user_123", - container_tags=["user_123", "project_123"], - custom_id="mem_abc123", - metadata={ - "category": "technology", - "isPublic": True, - "readingTime": 5, - "source": "web", - "tag_1": "ai", - "tag_2": "machine-learning", - }, + content="content", + container_tag="containerTag", + container_tags=["string"], + custom_id="customId", + metadata={"foo": "string"}, ) assert_matches_type(MemoryAddResponse, memory, path=["response"]) @@ -216,7 +209,7 @@ def test_method_add_with_all_params(self, client: Supermemory) -> None: @parametrize def test_raw_response_add(self, client: Supermemory) -> None: response = client.memories.with_raw_response.add( - content="This is a detailed article about machine learning concepts...", + content="content", ) assert response.is_closed is True @@ -228,7 +221,7 @@ def test_raw_response_add(self, client: Supermemory) -> None: @parametrize def test_streaming_response_add(self, client: Supermemory) -> None: with client.memories.with_streaming_response.add( - content="This is a detailed article about machine learning concepts...", + content="content", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -498,7 +491,7 @@ async def test_path_params_delete(self, async_client: AsyncSupermemory) -> None: @parametrize async def test_method_add(self, async_client: AsyncSupermemory) -> None: memory = await async_client.memories.add( - content="This is a detailed article about machine learning concepts...", + content="content", ) assert_matches_type(MemoryAddResponse, memory, path=["response"]) @@ -506,18 +499,11 @@ async def test_method_add(self, async_client: AsyncSupermemory) -> None: @parametrize async def test_method_add_with_all_params(self, async_client: AsyncSupermemory) -> None: memory = await async_client.memories.add( - content="This is a detailed article about machine learning concepts...", - container_tag="user_123", - container_tags=["user_123", "project_123"], - custom_id="mem_abc123", - metadata={ - "category": "technology", - "isPublic": True, - "readingTime": 5, - "source": "web", - "tag_1": "ai", - "tag_2": "machine-learning", - }, + content="content", + container_tag="containerTag", + container_tags=["string"], + custom_id="customId", + metadata={"foo": "string"}, ) assert_matches_type(MemoryAddResponse, memory, path=["response"]) @@ -525,7 +511,7 @@ async def test_method_add_with_all_params(self, async_client: AsyncSupermemory) @parametrize async def test_raw_response_add(self, async_client: AsyncSupermemory) -> None: response = await async_client.memories.with_raw_response.add( - content="This is a detailed article about machine learning concepts...", + content="content", ) assert response.is_closed is True @@ -537,7 +523,7 @@ async def test_raw_response_add(self, async_client: AsyncSupermemory) -> None: @parametrize async def test_streaming_response_add(self, async_client: AsyncSupermemory) -> None: async with async_client.memories.with_streaming_response.add( - content="This is a detailed article about machine learning concepts...", + content="content", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/test_client.py b/tests/test_client.py index 4e97c388..921e1144 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -727,9 +727,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien respx_mock.post("/v3/documents").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - client.memories.with_streaming_response.add( - content="This is a detailed article about machine learning concepts..." - ).__enter__() + client.memories.with_streaming_response.add(content="content").__enter__() assert _get_open_connections(self.client) == 0 @@ -739,9 +737,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client respx_mock.post("/v3/documents").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - client.memories.with_streaming_response.add( - content="This is a detailed article about machine learning concepts..." - ).__enter__() + client.memories.with_streaming_response.add(content="content").__enter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -770,9 +766,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v3/documents").mock(side_effect=retry_handler) - response = client.memories.with_raw_response.add( - content="This is a detailed article about machine learning concepts..." - ) + response = client.memories.with_raw_response.add(content="content") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -797,8 +791,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v3/documents").mock(side_effect=retry_handler) response = client.memories.with_raw_response.add( - content="This is a detailed article about machine learning concepts...", - extra_headers={"x-stainless-retry-count": Omit()}, + content="content", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -823,8 +816,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v3/documents").mock(side_effect=retry_handler) response = client.memories.with_raw_response.add( - content="This is a detailed article about machine learning concepts...", - extra_headers={"x-stainless-retry-count": "42"}, + content="content", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" @@ -1556,9 +1548,7 @@ async def test_retrying_timeout_errors_doesnt_leak( respx_mock.post("/v3/documents").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): - await async_client.memories.with_streaming_response.add( - content="This is a detailed article about machine learning concepts..." - ).__aenter__() + await async_client.memories.with_streaming_response.add(content="content").__aenter__() assert _get_open_connections(self.client) == 0 @@ -1570,9 +1560,7 @@ async def test_retrying_status_errors_doesnt_leak( respx_mock.post("/v3/documents").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): - await async_client.memories.with_streaming_response.add( - content="This is a detailed article about machine learning concepts..." - ).__aenter__() + await async_client.memories.with_streaming_response.add(content="content").__aenter__() assert _get_open_connections(self.client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @@ -1602,9 +1590,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v3/documents").mock(side_effect=retry_handler) - response = await client.memories.with_raw_response.add( - content="This is a detailed article about machine learning concepts..." - ) + response = await client.memories.with_raw_response.add(content="content") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1630,8 +1616,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v3/documents").mock(side_effect=retry_handler) response = await client.memories.with_raw_response.add( - content="This is a detailed article about machine learning concepts...", - extra_headers={"x-stainless-retry-count": Omit()}, + content="content", extra_headers={"x-stainless-retry-count": Omit()} ) assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 @@ -1657,8 +1642,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: respx_mock.post("/v3/documents").mock(side_effect=retry_handler) response = await client.memories.with_raw_response.add( - content="This is a detailed article about machine learning concepts...", - extra_headers={"x-stainless-retry-count": "42"}, + content="content", extra_headers={"x-stainless-retry-count": "42"} ) assert response.http_request.headers.get("x-stainless-retry-count") == "42" From 7bad0fc387de50f25bec0e3a7ecca2b732294460 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 03:03:20 +0000 Subject: [PATCH 10/23] chore(internal/tests): avoid race condition with implicit client cleanup --- tests/test_client.py | 364 ++++++++++++++++++++++++------------------- 1 file changed, 200 insertions(+), 164 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 921e1144..aa1ea71c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -59,51 +59,49 @@ def _get_open_connections(client: Supermemory | AsyncSupermemory) -> int: class TestSupermemory: - client = Supermemory(base_url=base_url, api_key=api_key, _strict_response_validation=True) - @pytest.mark.respx(base_url=base_url) - def test_raw_response(self, respx_mock: MockRouter) -> None: + def test_raw_response(self, respx_mock: MockRouter, client: Supermemory) -> None: respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = self.client.post("/foo", cast_to=httpx.Response) + response = client.post("/foo", cast_to=httpx.Response) assert response.status_code == 200 assert isinstance(response, httpx.Response) assert response.json() == {"foo": "bar"} @pytest.mark.respx(base_url=base_url) - def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None: + def test_raw_response_for_binary(self, respx_mock: MockRouter, client: Supermemory) -> None: respx_mock.post("/foo").mock( return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') ) - response = self.client.post("/foo", cast_to=httpx.Response) + response = client.post("/foo", cast_to=httpx.Response) assert response.status_code == 200 assert isinstance(response, httpx.Response) assert response.json() == {"foo": "bar"} - def test_copy(self) -> None: - copied = self.client.copy() - assert id(copied) != id(self.client) + def test_copy(self, client: Supermemory) -> None: + copied = client.copy() + assert id(copied) != id(client) - copied = self.client.copy(api_key="another My API Key") + copied = client.copy(api_key="another My API Key") assert copied.api_key == "another My API Key" - assert self.client.api_key == "My API Key" + assert client.api_key == "My API Key" - def test_copy_default_options(self) -> None: + def test_copy_default_options(self, client: Supermemory) -> None: # options that have a default are overridden correctly - copied = self.client.copy(max_retries=7) + copied = client.copy(max_retries=7) assert copied.max_retries == 7 - assert self.client.max_retries == 2 + assert client.max_retries == 2 copied2 = copied.copy(max_retries=6) assert copied2.max_retries == 6 assert copied.max_retries == 7 # timeout - assert isinstance(self.client.timeout, httpx.Timeout) - copied = self.client.copy(timeout=None) + assert isinstance(client.timeout, httpx.Timeout) + copied = client.copy(timeout=None) assert copied.timeout is None - assert isinstance(self.client.timeout, httpx.Timeout) + assert isinstance(client.timeout, httpx.Timeout) def test_copy_default_headers(self) -> None: client = Supermemory( @@ -138,6 +136,7 @@ def test_copy_default_headers(self) -> None: match="`default_headers` and `set_default_headers` arguments are mutually exclusive", ): client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) + client.close() def test_copy_default_query(self) -> None: client = Supermemory( @@ -175,13 +174,15 @@ def test_copy_default_query(self) -> None: ): client.copy(set_default_query={}, default_query={"foo": "Bar"}) - def test_copy_signature(self) -> None: + client.close() + + def test_copy_signature(self, client: Supermemory) -> None: # ensure the same parameters that can be passed to the client are defined in the `.copy()` method init_signature = inspect.signature( # mypy doesn't like that we access the `__init__` property. - self.client.__init__, # type: ignore[misc] + client.__init__, # type: ignore[misc] ) - copy_signature = inspect.signature(self.client.copy) + copy_signature = inspect.signature(client.copy) exclude_params = {"transport", "proxies", "_strict_response_validation"} for name in init_signature.parameters.keys(): @@ -192,12 +193,12 @@ def test_copy_signature(self) -> None: 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: + def test_copy_build_request(self, client: Supermemory) -> None: options = FinalRequestOptions(method="get", url="/foo") def build_request(options: FinalRequestOptions) -> None: - client = self.client.copy() - client._build_request(options) + client_copy = client.copy() + client_copy._build_request(options) # ensure that the machinery is warmed up before tracing starts. build_request(options) @@ -254,14 +255,12 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic print(frame) raise AssertionError() - def test_request_timeout(self) -> None: - request = self.client._build_request(FinalRequestOptions(method="get", url="/foo")) + def test_request_timeout(self, client: Supermemory) -> None: + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT - request = self.client._build_request( - FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)) - ) + request = client._build_request(FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == httpx.Timeout(100.0) @@ -274,6 +273,8 @@ def test_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == httpx.Timeout(0) + client.close() + def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used with httpx.Client(timeout=None) as http_client: @@ -285,6 +286,8 @@ def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == httpx.Timeout(None) + client.close() + # no timeout given to the httpx client should not use the httpx default with httpx.Client() as http_client: client = Supermemory( @@ -295,6 +298,8 @@ def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT + client.close() + # explicitly passing the default timeout currently results in it being ignored with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: client = Supermemory( @@ -305,6 +310,8 @@ def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT # our default + client.close() + async def test_invalid_http_client(self) -> None: with pytest.raises(TypeError, match="Invalid `http_client` arg"): async with httpx.AsyncClient() as http_client: @@ -316,14 +323,14 @@ async def test_invalid_http_client(self) -> None: ) def test_default_headers_option(self) -> None: - client = Supermemory( + test_client = Supermemory( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + request = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "bar" assert request.headers.get("x-stainless-lang") == "python" - client2 = Supermemory( + test_client2 = Supermemory( base_url=base_url, api_key=api_key, _strict_response_validation=True, @@ -332,10 +339,13 @@ def test_default_headers_option(self) -> None: "X-Stainless-Lang": "my-overriding-header", }, ) - request = client2._build_request(FinalRequestOptions(method="get", url="/foo")) + request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "stainless" assert request.headers.get("x-stainless-lang") == "my-overriding-header" + test_client.close() + test_client2.close() + def test_validate_headers(self) -> None: client = Supermemory(base_url=base_url, api_key=api_key, _strict_response_validation=True) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -364,8 +374,10 @@ def test_default_query_option(self) -> None: url = httpx.URL(request.url) assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} - def test_request_extra_json(self) -> None: - request = self.client._build_request( + client.close() + + def test_request_extra_json(self, client: Supermemory) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -376,7 +388,7 @@ def test_request_extra_json(self) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": False} - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -387,7 +399,7 @@ def test_request_extra_json(self) -> None: assert data == {"baz": False} # `extra_json` takes priority over `json_data` when keys clash - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -398,8 +410,8 @@ def test_request_extra_json(self) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": None} - def test_request_extra_headers(self) -> None: - request = self.client._build_request( + def test_request_extra_headers(self, client: Supermemory) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -409,7 +421,7 @@ def test_request_extra_headers(self) -> None: assert request.headers.get("X-Foo") == "Foo" # `extra_headers` takes priority over `default_headers` when keys clash - request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request( + request = client.with_options(default_headers={"X-Bar": "true"})._build_request( FinalRequestOptions( method="post", url="/foo", @@ -420,8 +432,8 @@ def test_request_extra_headers(self) -> None: ) assert request.headers.get("X-Bar") == "false" - def test_request_extra_query(self) -> None: - request = self.client._build_request( + def test_request_extra_query(self, client: Supermemory) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -434,7 +446,7 @@ def test_request_extra_query(self) -> None: assert params == {"my_query_param": "Foo"} # if both `query` and `extra_query` are given, they are merged - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -448,7 +460,7 @@ def test_request_extra_query(self) -> None: assert params == {"bar": "1", "foo": "2"} # `extra_query` takes priority over `query` when keys clash - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -491,7 +503,7 @@ def test_multipart_repeating_array(self, client: Supermemory) -> None: ] @pytest.mark.respx(base_url=base_url) - def test_basic_union_response(self, respx_mock: MockRouter) -> None: + def test_basic_union_response(self, respx_mock: MockRouter, client: Supermemory) -> None: class Model1(BaseModel): name: str @@ -500,12 +512,12 @@ class Model2(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model2) assert response.foo == "bar" @pytest.mark.respx(base_url=base_url) - def test_union_response_different_types(self, respx_mock: MockRouter) -> None: + def test_union_response_different_types(self, respx_mock: MockRouter, client: Supermemory) -> None: """Union of objects with the same field name using a different type""" class Model1(BaseModel): @@ -516,18 +528,18 @@ class Model2(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model2) assert response.foo == "bar" respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1})) - response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model1) assert response.foo == 1 @pytest.mark.respx(base_url=base_url) - def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None: + def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter, client: Supermemory) -> None: """ Response that sets Content-Type to something other than application/json but returns json data """ @@ -543,7 +555,7 @@ class Model(BaseModel): ) ) - response = self.client.get("/foo", cast_to=Model) + response = client.get("/foo", cast_to=Model) assert isinstance(response, Model) assert response.foo == 2 @@ -557,6 +569,8 @@ def test_base_url_setter(self) -> None: assert client.base_url == "https://example.com/from_setter/" + client.close() + def test_base_url_env(self) -> None: with update_env(SUPERMEMORY_BASE_URL="http://localhost:5000/from/env"): client = Supermemory(api_key=api_key, _strict_response_validation=True) @@ -586,6 +600,7 @@ def test_base_url_trailing_slash(self, client: Supermemory) -> None: ), ) assert request.url == "http://localhost:5000/custom/path/foo" + client.close() @pytest.mark.parametrize( "client", @@ -611,6 +626,7 @@ def test_base_url_no_trailing_slash(self, client: Supermemory) -> None: ), ) assert request.url == "http://localhost:5000/custom/path/foo" + client.close() @pytest.mark.parametrize( "client", @@ -636,35 +652,36 @@ def test_absolute_request_url(self, client: Supermemory) -> None: ), ) assert request.url == "https://myapi.com/foo" + client.close() def test_copied_client_does_not_close_http(self) -> None: - client = Supermemory(base_url=base_url, api_key=api_key, _strict_response_validation=True) - assert not client.is_closed() + test_client = Supermemory(base_url=base_url, api_key=api_key, _strict_response_validation=True) + assert not test_client.is_closed() - copied = client.copy() - assert copied is not client + copied = test_client.copy() + assert copied is not test_client del copied - assert not client.is_closed() + assert not test_client.is_closed() def test_client_context_manager(self) -> None: - client = Supermemory(base_url=base_url, api_key=api_key, _strict_response_validation=True) - with client as c2: - assert c2 is client + test_client = Supermemory(base_url=base_url, api_key=api_key, _strict_response_validation=True) + with test_client as c2: + assert c2 is test_client assert not c2.is_closed() - assert not client.is_closed() - assert client.is_closed() + assert not test_client.is_closed() + assert test_client.is_closed() @pytest.mark.respx(base_url=base_url) - def test_client_response_validation_error(self, respx_mock: MockRouter) -> None: + def test_client_response_validation_error(self, respx_mock: MockRouter, client: Supermemory) -> None: class Model(BaseModel): foo: str respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}})) with pytest.raises(APIResponseValidationError) as exc: - self.client.get("/foo", cast_to=Model) + client.get("/foo", cast_to=Model) assert isinstance(exc.value.__cause__, ValidationError) @@ -686,11 +703,14 @@ class Model(BaseModel): with pytest.raises(APIResponseValidationError): strict_client.get("/foo", cast_to=Model) - client = Supermemory(base_url=base_url, api_key=api_key, _strict_response_validation=False) + non_strict_client = Supermemory(base_url=base_url, api_key=api_key, _strict_response_validation=False) - response = client.get("/foo", cast_to=Model) + response = non_strict_client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] + strict_client.close() + non_strict_client.close() + @pytest.mark.parametrize( "remaining_retries,retry_after,timeout", [ @@ -713,9 +733,9 @@ class Model(BaseModel): ], ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) - def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: - client = Supermemory(base_url=base_url, api_key=api_key, _strict_response_validation=True) - + def test_parse_retry_after_header( + self, remaining_retries: int, retry_after: str, timeout: float, client: Supermemory + ) -> None: headers = httpx.Headers({"retry-after": retry_after}) options = FinalRequestOptions(method="get", url="/foo", max_retries=3) calculated = client._calculate_retry_timeout(remaining_retries, options, headers) @@ -729,7 +749,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien with pytest.raises(APITimeoutError): client.memories.with_streaming_response.add(content="content").__enter__() - assert _get_open_connections(self.client) == 0 + assert _get_open_connections(client) == 0 @mock.patch("supermemory._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) @@ -738,7 +758,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client with pytest.raises(APIStatusError): client.memories.with_streaming_response.add(content="content").__enter__() - assert _get_open_connections(self.client) == 0 + assert _get_open_connections(client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("supermemory._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @@ -844,83 +864,77 @@ def test_default_client_creation(self) -> None: ) @pytest.mark.respx(base_url=base_url) - def test_follow_redirects(self, respx_mock: MockRouter) -> None: + def test_follow_redirects(self, respx_mock: MockRouter, client: Supermemory) -> None: # Test that the default follow_redirects=True allows following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) ) respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) - response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + response = client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) assert response.status_code == 200 assert response.json() == {"status": "ok"} @pytest.mark.respx(base_url=base_url) - def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: + def test_follow_redirects_disabled(self, respx_mock: MockRouter, client: Supermemory) -> None: # Test that follow_redirects=False prevents following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) ) with pytest.raises(APIStatusError) as exc_info: - self.client.post( - "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response - ) + client.post("/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response) assert exc_info.value.response.status_code == 302 assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" class TestAsyncSupermemory: - client = AsyncSupermemory(base_url=base_url, api_key=api_key, _strict_response_validation=True) - @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio - async def test_raw_response(self, respx_mock: MockRouter) -> None: + async def test_raw_response(self, respx_mock: MockRouter, async_client: AsyncSupermemory) -> None: respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = await self.client.post("/foo", cast_to=httpx.Response) + response = await async_client.post("/foo", cast_to=httpx.Response) assert response.status_code == 200 assert isinstance(response, httpx.Response) assert response.json() == {"foo": "bar"} @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio - async def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None: + async def test_raw_response_for_binary(self, respx_mock: MockRouter, async_client: AsyncSupermemory) -> None: respx_mock.post("/foo").mock( return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') ) - response = await self.client.post("/foo", cast_to=httpx.Response) + response = await async_client.post("/foo", cast_to=httpx.Response) assert response.status_code == 200 assert isinstance(response, httpx.Response) assert response.json() == {"foo": "bar"} - def test_copy(self) -> None: - copied = self.client.copy() - assert id(copied) != id(self.client) + def test_copy(self, async_client: AsyncSupermemory) -> None: + copied = async_client.copy() + assert id(copied) != id(async_client) - copied = self.client.copy(api_key="another My API Key") + copied = async_client.copy(api_key="another My API Key") assert copied.api_key == "another My API Key" - assert self.client.api_key == "My API Key" + assert async_client.api_key == "My API Key" - def test_copy_default_options(self) -> None: + def test_copy_default_options(self, async_client: AsyncSupermemory) -> None: # options that have a default are overridden correctly - copied = self.client.copy(max_retries=7) + copied = async_client.copy(max_retries=7) assert copied.max_retries == 7 - assert self.client.max_retries == 2 + assert async_client.max_retries == 2 copied2 = copied.copy(max_retries=6) assert copied2.max_retries == 6 assert copied.max_retries == 7 # timeout - assert isinstance(self.client.timeout, httpx.Timeout) - copied = self.client.copy(timeout=None) + assert isinstance(async_client.timeout, httpx.Timeout) + copied = async_client.copy(timeout=None) assert copied.timeout is None - assert isinstance(self.client.timeout, httpx.Timeout) + assert isinstance(async_client.timeout, httpx.Timeout) - def test_copy_default_headers(self) -> None: + async def test_copy_default_headers(self) -> None: client = AsyncSupermemory( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) @@ -953,8 +967,9 @@ def test_copy_default_headers(self) -> None: match="`default_headers` and `set_default_headers` arguments are mutually exclusive", ): client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) + await client.close() - def test_copy_default_query(self) -> None: + async def test_copy_default_query(self) -> None: client = AsyncSupermemory( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} ) @@ -990,13 +1005,15 @@ def test_copy_default_query(self) -> None: ): client.copy(set_default_query={}, default_query={"foo": "Bar"}) - def test_copy_signature(self) -> None: + await client.close() + + def test_copy_signature(self, async_client: AsyncSupermemory) -> None: # ensure the same parameters that can be passed to the client are defined in the `.copy()` method init_signature = inspect.signature( # mypy doesn't like that we access the `__init__` property. - self.client.__init__, # type: ignore[misc] + async_client.__init__, # type: ignore[misc] ) - copy_signature = inspect.signature(self.client.copy) + copy_signature = inspect.signature(async_client.copy) exclude_params = {"transport", "proxies", "_strict_response_validation"} for name in init_signature.parameters.keys(): @@ -1007,12 +1024,12 @@ def test_copy_signature(self) -> None: 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: + def test_copy_build_request(self, async_client: AsyncSupermemory) -> None: options = FinalRequestOptions(method="get", url="/foo") def build_request(options: FinalRequestOptions) -> None: - client = self.client.copy() - client._build_request(options) + client_copy = async_client.copy() + client_copy._build_request(options) # ensure that the machinery is warmed up before tracing starts. build_request(options) @@ -1069,12 +1086,12 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic print(frame) raise AssertionError() - async def test_request_timeout(self) -> None: - request = self.client._build_request(FinalRequestOptions(method="get", url="/foo")) + async def test_request_timeout(self, async_client: AsyncSupermemory) -> None: + request = async_client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT - request = self.client._build_request( + request = async_client._build_request( FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)) ) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore @@ -1089,6 +1106,8 @@ async def test_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == httpx.Timeout(0) + await client.close() + async def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used async with httpx.AsyncClient(timeout=None) as http_client: @@ -1100,6 +1119,8 @@ async def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == httpx.Timeout(None) + await client.close() + # no timeout given to the httpx client should not use the httpx default async with httpx.AsyncClient() as http_client: client = AsyncSupermemory( @@ -1110,6 +1131,8 @@ async def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT + await client.close() + # explicitly passing the default timeout currently results in it being ignored async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: client = AsyncSupermemory( @@ -1120,6 +1143,8 @@ async def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT # our default + await client.close() + def test_invalid_http_client(self) -> None: with pytest.raises(TypeError, match="Invalid `http_client` arg"): with httpx.Client() as http_client: @@ -1130,15 +1155,15 @@ def test_invalid_http_client(self) -> None: http_client=cast(Any, http_client), ) - def test_default_headers_option(self) -> None: - client = AsyncSupermemory( + async def test_default_headers_option(self) -> None: + test_client = AsyncSupermemory( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + request = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "bar" assert request.headers.get("x-stainless-lang") == "python" - client2 = AsyncSupermemory( + test_client2 = AsyncSupermemory( base_url=base_url, api_key=api_key, _strict_response_validation=True, @@ -1147,10 +1172,13 @@ def test_default_headers_option(self) -> None: "X-Stainless-Lang": "my-overriding-header", }, ) - request = client2._build_request(FinalRequestOptions(method="get", url="/foo")) + request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "stainless" assert request.headers.get("x-stainless-lang") == "my-overriding-header" + await test_client.close() + await test_client2.close() + def test_validate_headers(self) -> None: client = AsyncSupermemory(base_url=base_url, api_key=api_key, _strict_response_validation=True) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1161,7 +1189,7 @@ def test_validate_headers(self) -> None: client2 = AsyncSupermemory(base_url=base_url, api_key=None, _strict_response_validation=True) _ = client2 - def test_default_query_option(self) -> None: + async def test_default_query_option(self) -> None: client = AsyncSupermemory( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} ) @@ -1179,8 +1207,10 @@ def test_default_query_option(self) -> None: url = httpx.URL(request.url) assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} - def test_request_extra_json(self) -> None: - request = self.client._build_request( + await client.close() + + def test_request_extra_json(self, client: Supermemory) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1191,7 +1221,7 @@ def test_request_extra_json(self) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": False} - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1202,7 +1232,7 @@ def test_request_extra_json(self) -> None: assert data == {"baz": False} # `extra_json` takes priority over `json_data` when keys clash - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1213,8 +1243,8 @@ def test_request_extra_json(self) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": None} - def test_request_extra_headers(self) -> None: - request = self.client._build_request( + def test_request_extra_headers(self, client: Supermemory) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1224,7 +1254,7 @@ def test_request_extra_headers(self) -> None: assert request.headers.get("X-Foo") == "Foo" # `extra_headers` takes priority over `default_headers` when keys clash - request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request( + request = client.with_options(default_headers={"X-Bar": "true"})._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1235,8 +1265,8 @@ def test_request_extra_headers(self) -> None: ) assert request.headers.get("X-Bar") == "false" - def test_request_extra_query(self) -> None: - request = self.client._build_request( + def test_request_extra_query(self, client: Supermemory) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1249,7 +1279,7 @@ def test_request_extra_query(self) -> None: assert params == {"my_query_param": "Foo"} # if both `query` and `extra_query` are given, they are merged - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1263,7 +1293,7 @@ def test_request_extra_query(self) -> None: assert params == {"bar": "1", "foo": "2"} # `extra_query` takes priority over `query` when keys clash - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1306,7 +1336,7 @@ def test_multipart_repeating_array(self, async_client: AsyncSupermemory) -> None ] @pytest.mark.respx(base_url=base_url) - async def test_basic_union_response(self, respx_mock: MockRouter) -> None: + async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncSupermemory) -> None: class Model1(BaseModel): name: str @@ -1315,12 +1345,12 @@ class Model2(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model2) assert response.foo == "bar" @pytest.mark.respx(base_url=base_url) - async def test_union_response_different_types(self, respx_mock: MockRouter) -> None: + async def test_union_response_different_types(self, respx_mock: MockRouter, async_client: AsyncSupermemory) -> None: """Union of objects with the same field name using a different type""" class Model1(BaseModel): @@ -1331,18 +1361,20 @@ class Model2(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model2) assert response.foo == "bar" respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1})) - response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model1) assert response.foo == 1 @pytest.mark.respx(base_url=base_url) - async def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None: + async def test_non_application_json_content_type_for_json_data( + self, respx_mock: MockRouter, async_client: AsyncSupermemory + ) -> None: """ Response that sets Content-Type to something other than application/json but returns json data """ @@ -1358,11 +1390,11 @@ class Model(BaseModel): ) ) - response = await self.client.get("/foo", cast_to=Model) + response = await async_client.get("/foo", cast_to=Model) assert isinstance(response, Model) assert response.foo == 2 - def test_base_url_setter(self) -> None: + async def test_base_url_setter(self) -> None: client = AsyncSupermemory( base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True ) @@ -1372,7 +1404,9 @@ def test_base_url_setter(self) -> None: assert client.base_url == "https://example.com/from_setter/" - def test_base_url_env(self) -> None: + await client.close() + + async def test_base_url_env(self) -> None: with update_env(SUPERMEMORY_BASE_URL="http://localhost:5000/from/env"): client = AsyncSupermemory(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" @@ -1392,7 +1426,7 @@ def test_base_url_env(self) -> None: ], ids=["standard", "custom http client"], ) - def test_base_url_trailing_slash(self, client: AsyncSupermemory) -> None: + async def test_base_url_trailing_slash(self, client: AsyncSupermemory) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1401,6 +1435,7 @@ def test_base_url_trailing_slash(self, client: AsyncSupermemory) -> None: ), ) assert request.url == "http://localhost:5000/custom/path/foo" + await client.close() @pytest.mark.parametrize( "client", @@ -1417,7 +1452,7 @@ def test_base_url_trailing_slash(self, client: AsyncSupermemory) -> None: ], ids=["standard", "custom http client"], ) - def test_base_url_no_trailing_slash(self, client: AsyncSupermemory) -> None: + async def test_base_url_no_trailing_slash(self, client: AsyncSupermemory) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1426,6 +1461,7 @@ def test_base_url_no_trailing_slash(self, client: AsyncSupermemory) -> None: ), ) assert request.url == "http://localhost:5000/custom/path/foo" + await client.close() @pytest.mark.parametrize( "client", @@ -1442,7 +1478,7 @@ def test_base_url_no_trailing_slash(self, client: AsyncSupermemory) -> None: ], ids=["standard", "custom http client"], ) - def test_absolute_request_url(self, client: AsyncSupermemory) -> None: + async def test_absolute_request_url(self, client: AsyncSupermemory) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1451,37 +1487,39 @@ def test_absolute_request_url(self, client: AsyncSupermemory) -> None: ), ) assert request.url == "https://myapi.com/foo" + await client.close() async def test_copied_client_does_not_close_http(self) -> None: - client = AsyncSupermemory(base_url=base_url, api_key=api_key, _strict_response_validation=True) - assert not client.is_closed() + test_client = AsyncSupermemory(base_url=base_url, api_key=api_key, _strict_response_validation=True) + assert not test_client.is_closed() - copied = client.copy() - assert copied is not client + copied = test_client.copy() + assert copied is not test_client del copied await asyncio.sleep(0.2) - assert not client.is_closed() + assert not test_client.is_closed() async def test_client_context_manager(self) -> None: - client = AsyncSupermemory(base_url=base_url, api_key=api_key, _strict_response_validation=True) - async with client as c2: - assert c2 is client + test_client = AsyncSupermemory(base_url=base_url, api_key=api_key, _strict_response_validation=True) + async with test_client as c2: + assert c2 is test_client assert not c2.is_closed() - assert not client.is_closed() - assert client.is_closed() + assert not test_client.is_closed() + assert test_client.is_closed() @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio - async def test_client_response_validation_error(self, respx_mock: MockRouter) -> None: + async def test_client_response_validation_error( + self, respx_mock: MockRouter, async_client: AsyncSupermemory + ) -> None: class Model(BaseModel): foo: str respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}})) with pytest.raises(APIResponseValidationError) as exc: - await self.client.get("/foo", cast_to=Model) + await async_client.get("/foo", cast_to=Model) assert isinstance(exc.value.__cause__, ValidationError) @@ -1492,7 +1530,6 @@ async def test_client_max_retries_validation(self) -> None: ) @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: class Model(BaseModel): name: str @@ -1504,11 +1541,14 @@ class Model(BaseModel): with pytest.raises(APIResponseValidationError): await strict_client.get("/foo", cast_to=Model) - client = AsyncSupermemory(base_url=base_url, api_key=api_key, _strict_response_validation=False) + non_strict_client = AsyncSupermemory(base_url=base_url, api_key=api_key, _strict_response_validation=False) - response = await client.get("/foo", cast_to=Model) + response = await non_strict_client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] + await strict_client.close() + await non_strict_client.close() + @pytest.mark.parametrize( "remaining_retries,retry_after,timeout", [ @@ -1531,13 +1571,12 @@ class Model(BaseModel): ], ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) - @pytest.mark.asyncio - async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: - client = AsyncSupermemory(base_url=base_url, api_key=api_key, _strict_response_validation=True) - + async def test_parse_retry_after_header( + self, remaining_retries: int, retry_after: str, timeout: float, async_client: AsyncSupermemory + ) -> None: headers = httpx.Headers({"retry-after": retry_after}) options = FinalRequestOptions(method="get", url="/foo", max_retries=3) - calculated = client._calculate_retry_timeout(remaining_retries, options, headers) + calculated = async_client._calculate_retry_timeout(remaining_retries, options, headers) assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] @mock.patch("supermemory._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @@ -1550,7 +1589,7 @@ async def test_retrying_timeout_errors_doesnt_leak( with pytest.raises(APITimeoutError): await async_client.memories.with_streaming_response.add(content="content").__aenter__() - assert _get_open_connections(self.client) == 0 + assert _get_open_connections(async_client) == 0 @mock.patch("supermemory._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) @@ -1561,12 +1600,11 @@ async def test_retrying_status_errors_doesnt_leak( with pytest.raises(APIStatusError): await async_client.memories.with_streaming_response.add(content="content").__aenter__() - assert _get_open_connections(self.client) == 0 + assert _get_open_connections(async_client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("supermemory._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio @pytest.mark.parametrize("failure_mode", ["status", "exception"]) async def test_retries_taken( self, @@ -1598,7 +1636,6 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("supermemory._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio async def test_omit_retry_count_header( self, async_client: AsyncSupermemory, failures_before_success: int, respx_mock: MockRouter ) -> None: @@ -1624,7 +1661,6 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("supermemory._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio async def test_overwrite_retry_count_header( self, async_client: AsyncSupermemory, failures_before_success: int, respx_mock: MockRouter ) -> None: @@ -1674,26 +1710,26 @@ async def test_default_client_creation(self) -> None: ) @pytest.mark.respx(base_url=base_url) - async def test_follow_redirects(self, respx_mock: MockRouter) -> None: + async def test_follow_redirects(self, respx_mock: MockRouter, async_client: AsyncSupermemory) -> None: # Test that the default follow_redirects=True allows following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) ) respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) - response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + response = await async_client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) assert response.status_code == 200 assert response.json() == {"status": "ok"} @pytest.mark.respx(base_url=base_url) - async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: + async def test_follow_redirects_disabled(self, respx_mock: MockRouter, async_client: AsyncSupermemory) -> None: # Test that follow_redirects=False prevents following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) ) with pytest.raises(APIStatusError) as exc_info: - await self.client.post( + await async_client.post( "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response ) From 329768ed8cec31b2ef510409b9f67a3f800a286b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 04:29:04 +0000 Subject: [PATCH 11/23] chore(internal): grammar fix (it's -> its) --- src/supermemory/_utils/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/supermemory/_utils/_utils.py b/src/supermemory/_utils/_utils.py index 50d59269..eec7f4a1 100644 --- a/src/supermemory/_utils/_utils.py +++ b/src/supermemory/_utils/_utils.py @@ -133,7 +133,7 @@ def is_given(obj: _T | NotGiven | Omit) -> TypeGuard[_T]: # Type safe methods for narrowing types with TypeVars. # The default narrowing for isinstance(obj, dict) is dict[unknown, unknown], # however this cause Pyright to rightfully report errors. As we know we don't -# care about the contained types we can safely use `object` in it's place. +# care about the contained types we can safely use `object` in its place. # # There are two separate functions defined, `is_*` and `is_*_t` for different use cases. # `is_*` is for when you're dealing with an unknown input From 3c3963845e949aa1755de61b1020f873c960b27b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 6 Nov 2025 00:21:55 +0000 Subject: [PATCH 12/23] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index b0d4915a..8bb9881e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/supermemory--inc%2Fsupermemory-new-4565eada92d3d7a0434e5896c283f460888b24f3ed5389f32bcbc2b104fb52df.yml -openapi_spec_hash: 124bf13a8b2727edb1b10dd2274cc2d2 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/supermemory--inc%2Fsupermemory-new-268046d0d3e461f90719c7aff95adde912763ec5dfdade1f0c25fc8a0cd9b25d.yml +openapi_spec_hash: cc8090e79852ab4566347b41faa409c0 config_hash: a478b24249ee4f53abfb5787ca4daf8b From 9feb588a112036bd5de9041f775232f7c1384e3f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 04:17:32 +0000 Subject: [PATCH 13/23] chore(package): drop Python 3.8 support --- README.md | 4 ++-- pyproject.toml | 5 ++--- src/supermemory/_utils/_sync.py | 34 +++------------------------------ 3 files changed, 7 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index b49bc5f7..9ed1ed88 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![PyPI version](https://img.shields.io/pypi/v/supermemory.svg?label=pypi%20(stable))](https://pypi.org/project/supermemory/) -The Supermemory Python library provides convenient access to the Supermemory REST API from any Python 3.8+ +The Supermemory Python library provides convenient access to the Supermemory REST API from any Python 3.9+ application. The library includes type definitions for all request params and response fields, and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). @@ -405,7 +405,7 @@ print(supermemory.__version__) ## Requirements -Python 3.8 or higher. +Python 3.9 or higher. ## Contributing diff --git a/pyproject.toml b/pyproject.toml index a88904ba..3a6081b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,11 +15,10 @@ dependencies = [ "distro>=1.7.0, <2", "sniffio", ] -requires-python = ">= 3.8" +requires-python = ">= 3.9" classifiers = [ "Typing :: Typed", "Intended Audience :: Developers", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -141,7 +140,7 @@ filterwarnings = [ # there are a couple of flags that are still disabled by # default in strict mode as they are experimental and niche. typeCheckingMode = "strict" -pythonVersion = "3.8" +pythonVersion = "3.9" exclude = [ "_dev", diff --git a/src/supermemory/_utils/_sync.py b/src/supermemory/_utils/_sync.py index ad7ec71b..f6027c18 100644 --- a/src/supermemory/_utils/_sync.py +++ b/src/supermemory/_utils/_sync.py @@ -1,10 +1,8 @@ from __future__ import annotations -import sys import asyncio import functools -import contextvars -from typing import Any, TypeVar, Callable, Awaitable +from typing import TypeVar, Callable, Awaitable from typing_extensions import ParamSpec import anyio @@ -15,34 +13,11 @@ T_ParamSpec = ParamSpec("T_ParamSpec") -if sys.version_info >= (3, 9): - _asyncio_to_thread = asyncio.to_thread -else: - # backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread - # for Python 3.8 support - async def _asyncio_to_thread( - func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs - ) -> Any: - """Asynchronously run function *func* in a separate thread. - - Any *args and **kwargs supplied for this function are directly passed - to *func*. Also, the current :class:`contextvars.Context` is propagated, - allowing context variables from the main thread to be accessed in the - separate thread. - - Returns a coroutine that can be awaited to get the eventual result of *func*. - """ - loop = asyncio.events.get_running_loop() - ctx = contextvars.copy_context() - func_call = functools.partial(ctx.run, func, *args, **kwargs) - return await loop.run_in_executor(None, func_call) - - async def to_thread( func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs ) -> T_Retval: if sniffio.current_async_library() == "asyncio": - return await _asyncio_to_thread(func, *args, **kwargs) + return await asyncio.to_thread(func, *args, **kwargs) return await anyio.to_thread.run_sync( functools.partial(func, *args, **kwargs), @@ -53,10 +28,7 @@ async def to_thread( def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: """ Take a blocking function and create an async one that receives the same - positional and keyword arguments. For python version 3.9 and above, it uses - asyncio.to_thread to run the function in a separate thread. For python version - 3.8, it uses locally defined copy of the asyncio.to_thread function which was - introduced in python 3.9. + positional and keyword arguments. Usage: From d42dd9c177546142c2d87484e1bb435401cfaac8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 04:18:10 +0000 Subject: [PATCH 14/23] fix: compat with Python 3.14 --- src/supermemory/_models.py | 11 ++++++++--- tests/test_models.py | 8 ++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/supermemory/_models.py b/src/supermemory/_models.py index 6a3cd1d2..fcec2cf9 100644 --- a/src/supermemory/_models.py +++ b/src/supermemory/_models.py @@ -2,6 +2,7 @@ import os import inspect +import weakref from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast from datetime import date, datetime from typing_extensions import ( @@ -573,6 +574,9 @@ class CachedDiscriminatorType(Protocol): __discriminator__: DiscriminatorDetails +DISCRIMINATOR_CACHE: weakref.WeakKeyDictionary[type, DiscriminatorDetails] = weakref.WeakKeyDictionary() + + class DiscriminatorDetails: field_name: str """The name of the discriminator field in the variant class, e.g. @@ -615,8 +619,9 @@ def __init__( def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, ...]) -> DiscriminatorDetails | None: - if isinstance(union, CachedDiscriminatorType): - return union.__discriminator__ + cached = DISCRIMINATOR_CACHE.get(union) + if cached is not None: + return cached discriminator_field_name: str | None = None @@ -669,7 +674,7 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, discriminator_field=discriminator_field_name, discriminator_alias=discriminator_alias, ) - cast(CachedDiscriminatorType, union).__discriminator__ = details + DISCRIMINATOR_CACHE.setdefault(union, details) return details diff --git a/tests/test_models.py b/tests/test_models.py index 3afd1f61..25b58530 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -9,7 +9,7 @@ from supermemory._utils import PropertyInfo from supermemory._compat import PYDANTIC_V1, parse_obj, model_dump, model_json -from supermemory._models import BaseModel, construct_type +from supermemory._models import DISCRIMINATOR_CACHE, BaseModel, construct_type class BasicModel(BaseModel): @@ -809,7 +809,7 @@ class B(BaseModel): UnionType = cast(Any, Union[A, B]) - assert not hasattr(UnionType, "__discriminator__") + assert not DISCRIMINATOR_CACHE.get(UnionType) m = construct_type( value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")]) @@ -818,7 +818,7 @@ class B(BaseModel): assert m.type == "b" assert m.data == "foo" # type: ignore[comparison-overlap] - discriminator = UnionType.__discriminator__ + discriminator = DISCRIMINATOR_CACHE.get(UnionType) assert discriminator is not None m = construct_type( @@ -830,7 +830,7 @@ class B(BaseModel): # if the discriminator details object stays the same between invocations then # we hit the cache - assert UnionType.__discriminator__ is discriminator + assert DISCRIMINATOR_CACHE.get(UnionType) is discriminator @pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1") From 447f54f0f7846283b4033275fe2042f6dff66a2c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 12 Nov 2025 04:05:35 +0000 Subject: [PATCH 15/23] fix(compat): update signatures of `model_dump` and `model_dump_json` for Pydantic v1 --- src/supermemory/_models.py | 41 +++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/supermemory/_models.py b/src/supermemory/_models.py index fcec2cf9..ca9500b2 100644 --- a/src/supermemory/_models.py +++ b/src/supermemory/_models.py @@ -257,15 +257,16 @@ def model_dump( mode: Literal["json", "python"] | str = "python", include: IncEx | None = None, exclude: IncEx | None = None, + context: Any | None = None, by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, + exclude_computed_fields: bool = False, round_trip: bool = False, warnings: bool | Literal["none", "warn", "error"] = True, - context: dict[str, Any] | None = None, - serialize_as_any: bool = False, fallback: Callable[[Any], Any] | None = None, + serialize_as_any: bool = False, ) -> dict[str, Any]: """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump @@ -273,16 +274,24 @@ def model_dump( Args: mode: The mode in which `to_python` should run. - If mode is 'json', the dictionary will only contain JSON serializable types. - If mode is 'python', the dictionary may contain any Python objects. - include: A list of fields to include in the output. - exclude: A list of fields to exclude from the output. + If mode is 'json', the output will only contain JSON serializable types. + If mode is 'python', the output may contain non-JSON-serializable Python objects. + include: A set of fields to include in the output. + exclude: A set of fields to exclude from the output. + context: Additional context to pass to the serializer. by_alias: Whether to use the field's alias in the dictionary key if defined. - exclude_unset: Whether to exclude fields that are unset or None from the output. - exclude_defaults: Whether to exclude fields that are set to their default value from the output. - exclude_none: Whether to exclude fields that have a value of `None` from the output. - round_trip: Whether to enable serialization and deserialization round-trip support. - warnings: Whether to log warnings when invalid fields are encountered. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that are set to their default value. + exclude_none: Whether to exclude fields that have a value of `None`. + exclude_computed_fields: Whether to exclude computed fields. + While this can be useful for round-tripping, it is usually recommended to use the dedicated + `round_trip` parameter instead. + round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T]. + warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors, + "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError]. + fallback: A function to call when an unknown value is encountered. If not provided, + a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised. + serialize_as_any: Whether to serialize fields with duck-typing serialization behavior. Returns: A dictionary representation of the model. @@ -299,6 +308,8 @@ def model_dump( raise ValueError("serialize_as_any is only supported in Pydantic v2") if fallback is not None: raise ValueError("fallback is only supported in Pydantic v2") + if exclude_computed_fields != False: + raise ValueError("exclude_computed_fields is only supported in Pydantic v2") dumped = super().dict( # pyright: ignore[reportDeprecated] include=include, exclude=exclude, @@ -315,15 +326,17 @@ def model_dump_json( self, *, indent: int | None = None, + ensure_ascii: bool = False, include: IncEx | None = None, exclude: IncEx | None = None, + context: Any | None = None, by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, + exclude_computed_fields: bool = False, round_trip: bool = False, warnings: bool | Literal["none", "warn", "error"] = True, - context: dict[str, Any] | None = None, fallback: Callable[[Any], Any] | None = None, serialize_as_any: bool = False, ) -> str: @@ -355,6 +368,10 @@ def model_dump_json( raise ValueError("serialize_as_any is only supported in Pydantic v2") if fallback is not None: raise ValueError("fallback is only supported in Pydantic v2") + if ensure_ascii != False: + raise ValueError("ensure_ascii is only supported in Pydantic v2") + if exclude_computed_fields != False: + raise ValueError("exclude_computed_fields is only supported in Pydantic v2") return super().json( # type: ignore[reportDeprecated] indent=indent, include=include, From e29337c11f26831d69e6f830542e5db708760b62 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 18 Nov 2025 22:21:40 +0000 Subject: [PATCH 16/23] feat(api): api update --- .stats.yml | 4 ++-- src/supermemory/resources/connections.py | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.stats.yml b/.stats.yml index 8bb9881e..364814fa 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/supermemory--inc%2Fsupermemory-new-268046d0d3e461f90719c7aff95adde912763ec5dfdade1f0c25fc8a0cd9b25d.yml -openapi_spec_hash: cc8090e79852ab4566347b41faa409c0 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/supermemory--inc%2Fsupermemory-new-ebd5e757d0e76cb83013e01a1e0bb3dba62beb83b2a2ffa28d148ea032e96fd0.yml +openapi_spec_hash: f930474a6ad230545154244045cc602e config_hash: a478b24249ee4f53abfb5787ca4daf8b diff --git a/src/supermemory/resources/connections.py b/src/supermemory/resources/connections.py index f56c31da..cec839e0 100644 --- a/src/supermemory/resources/connections.py +++ b/src/supermemory/resources/connections.py @@ -59,7 +59,7 @@ def with_streaming_response(self) -> ConnectionsResourceWithStreamingResponse: def create( self, - provider: Literal["notion", "google-drive", "onedrive"], + provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], *, container_tags: SequenceNotStr[str] | Omit = omit, document_limit: int | Omit = omit, @@ -172,7 +172,7 @@ def delete_by_id( def delete_by_provider( self, - provider: Literal["notion", "google-drive", "onedrive"], + provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], *, container_tags: SequenceNotStr[str], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -245,7 +245,7 @@ def get_by_id( def get_by_tags( self, - provider: Literal["notion", "google-drive", "onedrive"], + provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], *, container_tags: SequenceNotStr[str], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -284,7 +284,7 @@ def get_by_tags( def import_( self, - provider: Literal["notion", "google-drive", "onedrive"], + provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], *, container_tags: SequenceNotStr[str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -322,7 +322,7 @@ def import_( def list_documents( self, - provider: Literal["notion", "google-drive", "onedrive"], + provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], *, container_tags: SequenceNotStr[str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -382,7 +382,7 @@ def with_streaming_response(self) -> AsyncConnectionsResourceWithStreamingRespon async def create( self, - provider: Literal["notion", "google-drive", "onedrive"], + provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], *, container_tags: SequenceNotStr[str] | Omit = omit, document_limit: int | Omit = omit, @@ -497,7 +497,7 @@ async def delete_by_id( async def delete_by_provider( self, - provider: Literal["notion", "google-drive", "onedrive"], + provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], *, container_tags: SequenceNotStr[str], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -570,7 +570,7 @@ async def get_by_id( async def get_by_tags( self, - provider: Literal["notion", "google-drive", "onedrive"], + provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], *, container_tags: SequenceNotStr[str], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -609,7 +609,7 @@ async def get_by_tags( async def import_( self, - provider: Literal["notion", "google-drive", "onedrive"], + provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], *, container_tags: SequenceNotStr[str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -649,7 +649,7 @@ async def import_( async def list_documents( self, - provider: Literal["notion", "google-drive", "onedrive"], + provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], *, container_tags: SequenceNotStr[str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. From 0972c10cebf6bab936371dfa4c5ad87dad1f5496 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 20:21:19 +0000 Subject: [PATCH 17/23] feat(api): api update --- .stats.yml | 4 ++-- src/supermemory/resources/connections.py | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.stats.yml b/.stats.yml index 364814fa..8bb9881e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/supermemory--inc%2Fsupermemory-new-ebd5e757d0e76cb83013e01a1e0bb3dba62beb83b2a2ffa28d148ea032e96fd0.yml -openapi_spec_hash: f930474a6ad230545154244045cc602e +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/supermemory--inc%2Fsupermemory-new-268046d0d3e461f90719c7aff95adde912763ec5dfdade1f0c25fc8a0cd9b25d.yml +openapi_spec_hash: cc8090e79852ab4566347b41faa409c0 config_hash: a478b24249ee4f53abfb5787ca4daf8b diff --git a/src/supermemory/resources/connections.py b/src/supermemory/resources/connections.py index cec839e0..f56c31da 100644 --- a/src/supermemory/resources/connections.py +++ b/src/supermemory/resources/connections.py @@ -59,7 +59,7 @@ def with_streaming_response(self) -> ConnectionsResourceWithStreamingResponse: def create( self, - provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], + provider: Literal["notion", "google-drive", "onedrive"], *, container_tags: SequenceNotStr[str] | Omit = omit, document_limit: int | Omit = omit, @@ -172,7 +172,7 @@ def delete_by_id( def delete_by_provider( self, - provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], + provider: Literal["notion", "google-drive", "onedrive"], *, container_tags: SequenceNotStr[str], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -245,7 +245,7 @@ def get_by_id( def get_by_tags( self, - provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], + provider: Literal["notion", "google-drive", "onedrive"], *, container_tags: SequenceNotStr[str], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -284,7 +284,7 @@ def get_by_tags( def import_( self, - provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], + provider: Literal["notion", "google-drive", "onedrive"], *, container_tags: SequenceNotStr[str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -322,7 +322,7 @@ def import_( def list_documents( self, - provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], + provider: Literal["notion", "google-drive", "onedrive"], *, container_tags: SequenceNotStr[str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -382,7 +382,7 @@ def with_streaming_response(self) -> AsyncConnectionsResourceWithStreamingRespon async def create( self, - provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], + provider: Literal["notion", "google-drive", "onedrive"], *, container_tags: SequenceNotStr[str] | Omit = omit, document_limit: int | Omit = omit, @@ -497,7 +497,7 @@ async def delete_by_id( async def delete_by_provider( self, - provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], + provider: Literal["notion", "google-drive", "onedrive"], *, container_tags: SequenceNotStr[str], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -570,7 +570,7 @@ async def get_by_id( async def get_by_tags( self, - provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], + provider: Literal["notion", "google-drive", "onedrive"], *, container_tags: SequenceNotStr[str], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -609,7 +609,7 @@ async def get_by_tags( async def import_( self, - provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], + provider: Literal["notion", "google-drive", "onedrive"], *, container_tags: SequenceNotStr[str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -649,7 +649,7 @@ async def import_( async def list_documents( self, - provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], + provider: Literal["notion", "google-drive", "onedrive"], *, container_tags: SequenceNotStr[str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. From 8802bdd0529ef3759bf1b8547ce2405c8164d0e9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 22 Nov 2025 03:50:43 +0000 Subject: [PATCH 18/23] chore: add Python 3.14 classifier and testing --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 3a6081b7..71ccb3cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: MacOS", From a7749c97703d995ed97ba1a5c09fef9e5804bea7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 01:21:22 +0000 Subject: [PATCH 19/23] feat(api): api update --- .stats.yml | 4 ++-- src/supermemory/resources/connections.py | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.stats.yml b/.stats.yml index 8bb9881e..364814fa 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/supermemory--inc%2Fsupermemory-new-268046d0d3e461f90719c7aff95adde912763ec5dfdade1f0c25fc8a0cd9b25d.yml -openapi_spec_hash: cc8090e79852ab4566347b41faa409c0 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/supermemory--inc%2Fsupermemory-new-ebd5e757d0e76cb83013e01a1e0bb3dba62beb83b2a2ffa28d148ea032e96fd0.yml +openapi_spec_hash: f930474a6ad230545154244045cc602e config_hash: a478b24249ee4f53abfb5787ca4daf8b diff --git a/src/supermemory/resources/connections.py b/src/supermemory/resources/connections.py index f56c31da..cec839e0 100644 --- a/src/supermemory/resources/connections.py +++ b/src/supermemory/resources/connections.py @@ -59,7 +59,7 @@ def with_streaming_response(self) -> ConnectionsResourceWithStreamingResponse: def create( self, - provider: Literal["notion", "google-drive", "onedrive"], + provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], *, container_tags: SequenceNotStr[str] | Omit = omit, document_limit: int | Omit = omit, @@ -172,7 +172,7 @@ def delete_by_id( def delete_by_provider( self, - provider: Literal["notion", "google-drive", "onedrive"], + provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], *, container_tags: SequenceNotStr[str], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -245,7 +245,7 @@ def get_by_id( def get_by_tags( self, - provider: Literal["notion", "google-drive", "onedrive"], + provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], *, container_tags: SequenceNotStr[str], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -284,7 +284,7 @@ def get_by_tags( def import_( self, - provider: Literal["notion", "google-drive", "onedrive"], + provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], *, container_tags: SequenceNotStr[str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -322,7 +322,7 @@ def import_( def list_documents( self, - provider: Literal["notion", "google-drive", "onedrive"], + provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], *, container_tags: SequenceNotStr[str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -382,7 +382,7 @@ def with_streaming_response(self) -> AsyncConnectionsResourceWithStreamingRespon async def create( self, - provider: Literal["notion", "google-drive", "onedrive"], + provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], *, container_tags: SequenceNotStr[str] | Omit = omit, document_limit: int | Omit = omit, @@ -497,7 +497,7 @@ async def delete_by_id( async def delete_by_provider( self, - provider: Literal["notion", "google-drive", "onedrive"], + provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], *, container_tags: SequenceNotStr[str], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -570,7 +570,7 @@ async def get_by_id( async def get_by_tags( self, - provider: Literal["notion", "google-drive", "onedrive"], + provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], *, container_tags: SequenceNotStr[str], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -609,7 +609,7 @@ async def get_by_tags( async def import_( self, - provider: Literal["notion", "google-drive", "onedrive"], + provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], *, container_tags: SequenceNotStr[str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -649,7 +649,7 @@ async def import_( async def list_documents( self, - provider: Literal["notion", "google-drive", "onedrive"], + provider: Literal["notion", "google-drive", "onedrive", "web-crawler"], *, container_tags: SequenceNotStr[str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. From 1c5e2f915c7d380c9b2b212c35c5d95bbc7124ac Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 23:27:54 +0000 Subject: [PATCH 20/23] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 364814fa..eb07f309 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/supermemory--inc%2Fsupermemory-new-ebd5e757d0e76cb83013e01a1e0bb3dba62beb83b2a2ffa28d148ea032e96fd0.yml openapi_spec_hash: f930474a6ad230545154244045cc602e -config_hash: a478b24249ee4f53abfb5787ca4daf8b +config_hash: a467f397fc0a20a494d1c2acc620cacf From 5acbfb92591b918173358e0122c607ca87d9f994 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 23:28:55 +0000 Subject: [PATCH 21/23] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index eb07f309..e226a16f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/supermemory--inc%2Fsupermemory-new-ebd5e757d0e76cb83013e01a1e0bb3dba62beb83b2a2ffa28d148ea032e96fd0.yml openapi_spec_hash: f930474a6ad230545154244045cc602e -config_hash: a467f397fc0a20a494d1c2acc620cacf +config_hash: 4cd6c09cac62e96e5ab8a77a3fb1b2a4 From c780b0fc9afb857dd55613d5b67cd7849b68973c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 23:29:56 +0000 Subject: [PATCH 22/23] feat(api): manual updates --- .stats.yml | 4 +- api.md | 12 ++ src/supermemory/_client.py | 10 +- src/supermemory/resources/__init__.py | 14 ++ src/supermemory/resources/profile.py | 187 ++++++++++++++++++ src/supermemory/types/__init__.py | 2 + .../types/profile_property_params.py | 21 ++ .../types/profile_property_response.py | 35 ++++ tests/api_resources/test_profile.py | 110 +++++++++++ 9 files changed, 392 insertions(+), 3 deletions(-) create mode 100644 src/supermemory/resources/profile.py create mode 100644 src/supermemory/types/profile_property_params.py create mode 100644 src/supermemory/types/profile_property_response.py create mode 100644 tests/api_resources/test_profile.py diff --git a/.stats.yml b/.stats.yml index e226a16f..19a9a0b1 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 18 +configured_endpoints: 19 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/supermemory--inc%2Fsupermemory-new-ebd5e757d0e76cb83013e01a1e0bb3dba62beb83b2a2ffa28d148ea032e96fd0.yml openapi_spec_hash: f930474a6ad230545154244045cc602e -config_hash: 4cd6c09cac62e96e5ab8a77a3fb1b2a4 +config_hash: 5761a0b4f8c53c72efab21d41c00012b diff --git a/api.md b/api.md index 4451d8b1..0e4a1b7c 100644 --- a/api.md +++ b/api.md @@ -50,6 +50,18 @@ Methods: - client.documents.get(id) -> DocumentGetResponse - client.documents.upload_file(\*\*params) -> DocumentUploadFileResponse +# Profile + +Types: + +```python +from supermemory.types import ProfilePropertyResponse +``` + +Methods: + +- client.profile.property(\*\*params) -> ProfilePropertyResponse + # Search Types: diff --git a/src/supermemory/_client.py b/src/supermemory/_client.py index 0de41593..6982c404 100644 --- a/src/supermemory/_client.py +++ b/src/supermemory/_client.py @@ -21,7 +21,7 @@ ) from ._utils import is_given, get_async_library from ._version import __version__ -from .resources import search, memories, settings, documents, connections +from .resources import search, profile, memories, settings, documents, connections from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import APIStatusError, SupermemoryError from ._base_client import ( @@ -45,6 +45,7 @@ class Supermemory(SyncAPIClient): memories: memories.MemoriesResource documents: documents.DocumentsResource + profile: profile.ProfileResource search: search.SearchResource settings: settings.SettingsResource connections: connections.ConnectionsResource @@ -107,6 +108,7 @@ def __init__( self.memories = memories.MemoriesResource(self) self.documents = documents.DocumentsResource(self) + self.profile = profile.ProfileResource(self) self.search = search.SearchResource(self) self.settings = settings.SettingsResource(self) self.connections = connections.ConnectionsResource(self) @@ -221,6 +223,7 @@ def _make_status_error( class AsyncSupermemory(AsyncAPIClient): memories: memories.AsyncMemoriesResource documents: documents.AsyncDocumentsResource + profile: profile.AsyncProfileResource search: search.AsyncSearchResource settings: settings.AsyncSettingsResource connections: connections.AsyncConnectionsResource @@ -283,6 +286,7 @@ def __init__( self.memories = memories.AsyncMemoriesResource(self) self.documents = documents.AsyncDocumentsResource(self) + self.profile = profile.AsyncProfileResource(self) self.search = search.AsyncSearchResource(self) self.settings = settings.AsyncSettingsResource(self) self.connections = connections.AsyncConnectionsResource(self) @@ -398,6 +402,7 @@ class SupermemoryWithRawResponse: def __init__(self, client: Supermemory) -> None: self.memories = memories.MemoriesResourceWithRawResponse(client.memories) self.documents = documents.DocumentsResourceWithRawResponse(client.documents) + self.profile = profile.ProfileResourceWithRawResponse(client.profile) self.search = search.SearchResourceWithRawResponse(client.search) self.settings = settings.SettingsResourceWithRawResponse(client.settings) self.connections = connections.ConnectionsResourceWithRawResponse(client.connections) @@ -407,6 +412,7 @@ class AsyncSupermemoryWithRawResponse: def __init__(self, client: AsyncSupermemory) -> None: self.memories = memories.AsyncMemoriesResourceWithRawResponse(client.memories) self.documents = documents.AsyncDocumentsResourceWithRawResponse(client.documents) + self.profile = profile.AsyncProfileResourceWithRawResponse(client.profile) self.search = search.AsyncSearchResourceWithRawResponse(client.search) self.settings = settings.AsyncSettingsResourceWithRawResponse(client.settings) self.connections = connections.AsyncConnectionsResourceWithRawResponse(client.connections) @@ -416,6 +422,7 @@ class SupermemoryWithStreamedResponse: def __init__(self, client: Supermemory) -> None: self.memories = memories.MemoriesResourceWithStreamingResponse(client.memories) self.documents = documents.DocumentsResourceWithStreamingResponse(client.documents) + self.profile = profile.ProfileResourceWithStreamingResponse(client.profile) self.search = search.SearchResourceWithStreamingResponse(client.search) self.settings = settings.SettingsResourceWithStreamingResponse(client.settings) self.connections = connections.ConnectionsResourceWithStreamingResponse(client.connections) @@ -425,6 +432,7 @@ class AsyncSupermemoryWithStreamedResponse: def __init__(self, client: AsyncSupermemory) -> None: self.memories = memories.AsyncMemoriesResourceWithStreamingResponse(client.memories) self.documents = documents.AsyncDocumentsResourceWithStreamingResponse(client.documents) + self.profile = profile.AsyncProfileResourceWithStreamingResponse(client.profile) self.search = search.AsyncSearchResourceWithStreamingResponse(client.search) self.settings = settings.AsyncSettingsResourceWithStreamingResponse(client.settings) self.connections = connections.AsyncConnectionsResourceWithStreamingResponse(client.connections) diff --git a/src/supermemory/resources/__init__.py b/src/supermemory/resources/__init__.py index ca24f73f..14a2c145 100644 --- a/src/supermemory/resources/__init__.py +++ b/src/supermemory/resources/__init__.py @@ -8,6 +8,14 @@ SearchResourceWithStreamingResponse, AsyncSearchResourceWithStreamingResponse, ) +from .profile import ( + ProfileResource, + AsyncProfileResource, + ProfileResourceWithRawResponse, + AsyncProfileResourceWithRawResponse, + ProfileResourceWithStreamingResponse, + AsyncProfileResourceWithStreamingResponse, +) from .memories import ( MemoriesResource, AsyncMemoriesResource, @@ -54,6 +62,12 @@ "AsyncDocumentsResourceWithRawResponse", "DocumentsResourceWithStreamingResponse", "AsyncDocumentsResourceWithStreamingResponse", + "ProfileResource", + "AsyncProfileResource", + "ProfileResourceWithRawResponse", + "AsyncProfileResourceWithRawResponse", + "ProfileResourceWithStreamingResponse", + "AsyncProfileResourceWithStreamingResponse", "SearchResource", "AsyncSearchResource", "SearchResourceWithRawResponse", diff --git a/src/supermemory/resources/profile.py b/src/supermemory/resources/profile.py new file mode 100644 index 00000000..23ec2733 --- /dev/null +++ b/src/supermemory/resources/profile.py @@ -0,0 +1,187 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..types import profile_property_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +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.profile_property_response import ProfilePropertyResponse + +__all__ = ["ProfileResource", "AsyncProfileResource"] + + +class ProfileResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> ProfileResourceWithRawResponse: + """ + 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/supermemoryai/python-sdk#accessing-raw-response-data-eg-headers + """ + return ProfileResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ProfileResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/supermemoryai/python-sdk#with_streaming_response + """ + return ProfileResourceWithStreamingResponse(self) + + def property( + self, + *, + container_tag: str, + q: str | Omit = omit, + # 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, + ) -> ProfilePropertyResponse: + """ + Get user profile with optional search results + + Args: + container_tag: Tag to filter the profile by. This can be an ID for your user, a project ID, or + any other identifier you wish to use to filter memories. + + q: Optional search query to include search results in the response + + 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( + "/v4/profile", + body=maybe_transform( + { + "container_tag": container_tag, + "q": q, + }, + profile_property_params.ProfilePropertyParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProfilePropertyResponse, + ) + + +class AsyncProfileResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncProfileResourceWithRawResponse: + """ + 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/supermemoryai/python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncProfileResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncProfileResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/supermemoryai/python-sdk#with_streaming_response + """ + return AsyncProfileResourceWithStreamingResponse(self) + + async def property( + self, + *, + container_tag: str, + q: str | Omit = omit, + # 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, + ) -> ProfilePropertyResponse: + """ + Get user profile with optional search results + + Args: + container_tag: Tag to filter the profile by. This can be an ID for your user, a project ID, or + any other identifier you wish to use to filter memories. + + q: Optional search query to include search results in the response + + 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( + "/v4/profile", + body=await async_maybe_transform( + { + "container_tag": container_tag, + "q": q, + }, + profile_property_params.ProfilePropertyParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProfilePropertyResponse, + ) + + +class ProfileResourceWithRawResponse: + def __init__(self, profile: ProfileResource) -> None: + self._profile = profile + + self.property = to_raw_response_wrapper( + profile.property, + ) + + +class AsyncProfileResourceWithRawResponse: + def __init__(self, profile: AsyncProfileResource) -> None: + self._profile = profile + + self.property = async_to_raw_response_wrapper( + profile.property, + ) + + +class ProfileResourceWithStreamingResponse: + def __init__(self, profile: ProfileResource) -> None: + self._profile = profile + + self.property = to_streamed_response_wrapper( + profile.property, + ) + + +class AsyncProfileResourceWithStreamingResponse: + def __init__(self, profile: AsyncProfileResource) -> None: + self._profile = profile + + self.property = async_to_streamed_response_wrapper( + profile.property, + ) diff --git a/src/supermemory/types/__init__.py b/src/supermemory/types/__init__.py index 54bc0e85..cf59ae71 100644 --- a/src/supermemory/types/__init__.py +++ b/src/supermemory/types/__init__.py @@ -21,6 +21,7 @@ from .document_update_params import DocumentUpdateParams as DocumentUpdateParams from .memory_update_response import MemoryUpdateResponse as MemoryUpdateResponse from .search_memories_params import SearchMemoriesParams as SearchMemoriesParams +from .profile_property_params import ProfilePropertyParams as ProfilePropertyParams from .search_documents_params import SearchDocumentsParams as SearchDocumentsParams from .search_execute_response import SearchExecuteResponse as SearchExecuteResponse from .setting_update_response import SettingUpdateResponse as SettingUpdateResponse @@ -30,6 +31,7 @@ from .document_update_response import DocumentUpdateResponse as DocumentUpdateResponse from .search_memories_response import SearchMemoriesResponse as SearchMemoriesResponse from .memory_upload_file_params import MemoryUploadFileParams as MemoryUploadFileParams +from .profile_property_response import ProfilePropertyResponse as ProfilePropertyResponse from .search_documents_response import SearchDocumentsResponse as SearchDocumentsResponse from .connection_create_response import ConnectionCreateResponse as ConnectionCreateResponse from .connection_import_response import ConnectionImportResponse as ConnectionImportResponse diff --git a/src/supermemory/types/profile_property_params.py b/src/supermemory/types/profile_property_params.py new file mode 100644 index 00000000..46cb736d --- /dev/null +++ b/src/supermemory/types/profile_property_params.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["ProfilePropertyParams"] + + +class ProfilePropertyParams(TypedDict, total=False): + container_tag: Required[Annotated[str, PropertyInfo(alias="containerTag")]] + """Tag to filter the profile by. + + This can be an ID for your user, a project ID, or any other identifier you wish + to use to filter memories. + """ + + q: str + """Optional search query to include search results in the response""" diff --git a/src/supermemory/types/profile_property_response.py b/src/supermemory/types/profile_property_response.py new file mode 100644 index 00000000..6e7e1383 --- /dev/null +++ b/src/supermemory/types/profile_property_response.py @@ -0,0 +1,35 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["ProfilePropertyResponse", "Profile", "SearchResults"] + + +class Profile(BaseModel): + dynamic: List[str] + """Dynamic profile information (recent memories)""" + + static: List[str] + """Static profile information that remains relevant long-term""" + + +class SearchResults(BaseModel): + results: List[object] + """Search results for the provided query""" + + timing: float + """Search timing in milliseconds""" + + total: float + """Total number of search results""" + + +class ProfilePropertyResponse(BaseModel): + profile: Profile + + search_results: Optional[SearchResults] = FieldInfo(alias="searchResults", default=None) + """Search results if a search query was provided""" diff --git a/tests/api_resources/test_profile.py b/tests/api_resources/test_profile.py new file mode 100644 index 00000000..4312ae78 --- /dev/null +++ b/tests/api_resources/test_profile.py @@ -0,0 +1,110 @@ +# 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 supermemory import Supermemory, AsyncSupermemory +from tests.utils import assert_matches_type +from supermemory.types import ProfilePropertyResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestProfile: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_property(self, client: Supermemory) -> None: + profile = client.profile.property( + container_tag="containerTag", + ) + assert_matches_type(ProfilePropertyResponse, profile, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_property_with_all_params(self, client: Supermemory) -> None: + profile = client.profile.property( + container_tag="containerTag", + q="q", + ) + assert_matches_type(ProfilePropertyResponse, profile, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_property(self, client: Supermemory) -> None: + response = client.profile.with_raw_response.property( + container_tag="containerTag", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + profile = response.parse() + assert_matches_type(ProfilePropertyResponse, profile, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_property(self, client: Supermemory) -> None: + with client.profile.with_streaming_response.property( + container_tag="containerTag", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + profile = response.parse() + assert_matches_type(ProfilePropertyResponse, profile, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncProfile: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_property(self, async_client: AsyncSupermemory) -> None: + profile = await async_client.profile.property( + container_tag="containerTag", + ) + assert_matches_type(ProfilePropertyResponse, profile, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_property_with_all_params(self, async_client: AsyncSupermemory) -> None: + profile = await async_client.profile.property( + container_tag="containerTag", + q="q", + ) + assert_matches_type(ProfilePropertyResponse, profile, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_property(self, async_client: AsyncSupermemory) -> None: + response = await async_client.profile.with_raw_response.property( + container_tag="containerTag", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + profile = await response.parse() + assert_matches_type(ProfilePropertyResponse, profile, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_property(self, async_client: AsyncSupermemory) -> None: + async with async_client.profile.with_streaming_response.property( + container_tag="containerTag", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + profile = await response.parse() + assert_matches_type(ProfilePropertyResponse, profile, path=["response"]) + + assert cast(Any, response.is_closed) is True From 76ead3d1ef49b0f8bc2fa60b83cc02d63894c2ef Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 23:30:12 +0000 Subject: [PATCH 23/23] release: 3.5.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ pyproject.toml | 2 +- src/supermemory/_version.py | 2 +- 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2437b419..bf0d0361 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.4.0" + ".": "3.5.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e5282c2..abe4f578 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## 3.5.0 (2025-11-26) + +Full Changelog: [v3.4.0...v3.5.0](https://github.com/supermemoryai/python-sdk/compare/v3.4.0...v3.5.0) + +### Features + +* **api:** api update ([a7749c9](https://github.com/supermemoryai/python-sdk/commit/a7749c97703d995ed97ba1a5c09fef9e5804bea7)) +* **api:** api update ([0972c10](https://github.com/supermemoryai/python-sdk/commit/0972c10cebf6bab936371dfa4c5ad87dad1f5496)) +* **api:** api update ([e29337c](https://github.com/supermemoryai/python-sdk/commit/e29337c11f26831d69e6f830542e5db708760b62)) +* **api:** api update ([a776c2c](https://github.com/supermemoryai/python-sdk/commit/a776c2cfcf82ee239c707b9065bd3313755388fa)) +* **api:** api update ([4e6c8e6](https://github.com/supermemoryai/python-sdk/commit/4e6c8e61653c99c733f862cbb5e08478159d082b)) +* **api:** manual updates ([c780b0f](https://github.com/supermemoryai/python-sdk/commit/c780b0fc9afb857dd55613d5b67cd7849b68973c)) + + +### Bug Fixes + +* **client:** close streams without requiring full consumption ([1155c43](https://github.com/supermemoryai/python-sdk/commit/1155c4396baa8681dcb72f6b53f90c42052fac6f)) +* compat with Python 3.14 ([d42dd9c](https://github.com/supermemoryai/python-sdk/commit/d42dd9c177546142c2d87484e1bb435401cfaac8)) +* **compat:** update signatures of `model_dump` and `model_dump_json` for Pydantic v1 ([447f54f](https://github.com/supermemoryai/python-sdk/commit/447f54f0f7846283b4033275fe2042f6dff66a2c)) + + +### Chores + +* add Python 3.14 classifier and testing ([8802bdd](https://github.com/supermemoryai/python-sdk/commit/8802bdd0529ef3759bf1b8547ce2405c8164d0e9)) +* bump `httpx-aiohttp` version to 0.1.9 ([6ad4b61](https://github.com/supermemoryai/python-sdk/commit/6ad4b613df1d4177fabeaf0b67d984e195af8594)) +* **internal/tests:** avoid race condition with implicit client cleanup ([7bad0fc](https://github.com/supermemoryai/python-sdk/commit/7bad0fc387de50f25bec0e3a7ecca2b732294460)) +* **internal:** detect missing future annotations with ruff ([6085dd3](https://github.com/supermemoryai/python-sdk/commit/6085dd39d67398eec25d5b9bcc5371f4810622c8)) +* **internal:** grammar fix (it's -> its) ([329768e](https://github.com/supermemoryai/python-sdk/commit/329768ed8cec31b2ef510409b9f67a3f800a286b)) +* **package:** drop Python 3.8 support ([9feb588](https://github.com/supermemoryai/python-sdk/commit/9feb588a112036bd5de9041f775232f7c1384e3f)) + ## 3.4.0 (2025-10-07) Full Changelog: [v3.3.0...v3.4.0](https://github.com/supermemoryai/python-sdk/compare/v3.3.0...v3.4.0) diff --git a/pyproject.toml b/pyproject.toml index 71ccb3cc..9d073d2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "supermemory" -version = "3.4.0" +version = "3.5.0" description = "The official Python library for the supermemory API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/supermemory/_version.py b/src/supermemory/_version.py index 0cd74f95..8d8aec6f 100644 --- a/src/supermemory/_version.py +++ b/src/supermemory/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "supermemory" -__version__ = "3.4.0" # x-release-please-version +__version__ = "3.5.0" # x-release-please-version