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/.stats.yml b/.stats.yml
index ac4b130c..19a9a0b1 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
-config_hash: a478b24249ee4f53abfb5787ca4daf8b
+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: 5761a0b4f8c53c72efab21d41c00012b
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/README.md b/README.md
index 113af8d0..9ed1ed88 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
[)](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).
@@ -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"))
@@ -405,7 +405,7 @@ print(supermemory.__version__)
## Requirements
-Python 3.8 or higher.
+Python 3.9 or higher.
## Contributing
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/pyproject.toml b/pyproject.toml
index 31501529..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"
@@ -15,16 +15,16 @@ 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",
"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",
@@ -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
@@ -141,7 +141,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",
@@ -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"
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
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/_models.py b/src/supermemory/_models.py
index 6a3cd1d2..ca9500b2 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 (
@@ -256,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
@@ -272,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.
@@ -298,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,
@@ -314,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:
@@ -354,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,
@@ -573,6 +591,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 +636,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 +691,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/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
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:
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
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
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/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.
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/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/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/__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/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/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/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_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/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
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,
diff --git a/tests/test_client.py b/tests/test_client.py
index 4e97c388..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)
@@ -727,11 +747,9 @@ 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
+ 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)
@@ -739,10 +757,8 @@ 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__()
- assert _get_open_connections(self.client) == 0
+ client.memories.with_streaming_response.add(content="content").__enter__()
+ 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)
@@ -770,9 +786,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 +811,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 +836,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"
@@ -852,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"}
)
@@ -961,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"}
)
@@ -998,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():
@@ -1015,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)
@@ -1077,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
@@ -1097,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:
@@ -1108,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(
@@ -1118,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(
@@ -1128,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:
@@ -1138,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,
@@ -1155,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"))
@@ -1169,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"}
)
@@ -1187,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",
@@ -1199,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",
@@ -1210,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",
@@ -1221,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",
@@ -1232,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",
@@ -1243,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",
@@ -1257,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",
@@ -1271,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",
@@ -1314,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
@@ -1323,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):
@@ -1339,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
"""
@@ -1366,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
)
@@ -1380,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/"
@@ -1400,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",
@@ -1409,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",
@@ -1425,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",
@@ -1434,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",
@@ -1450,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",
@@ -1459,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)
@@ -1500,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
@@ -1512,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",
[
@@ -1539,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)
@@ -1556,11 +1587,9 @@ 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
+ 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)
@@ -1570,15 +1599,12 @@ 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__()
- assert _get_open_connections(self.client) == 0
+ await async_client.memories.with_streaming_response.add(content="content").__aenter__()
+ 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,
@@ -1602,9 +1628,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
@@ -1612,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:
@@ -1630,8 +1653,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
@@ -1639,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:
@@ -1657,8 +1678,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"
@@ -1690,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
)
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")