diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4ae7e4a0..ef0780c0 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.0.0-alpha.2" + ".": "3.0.0-alpha.19" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 8f056938..561e86c8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 8 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/supermemory--inc%2Fsupermemory-4c45e387cbdc7c80d75cdb8eb924cf92a3a48a0c10060fda917b83a7e454aef5.yml -openapi_spec_hash: c859ac2e3429ad3663337b99c722f317 +configured_endpoints: 9 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/supermemory--inc%2Fsupermemory-0e2874da641d72b5833ebef8cc792d86250d397b96eeedba7d4759ffabc076de.yml +openapi_spec_hash: f13ea02b49134e11025cb18f3d45d313 config_hash: 8477e3ee6fd596ab6ac911d052e4de79 diff --git a/CHANGELOG.md b/CHANGELOG.md index 68ff2aa2..f6bfd4b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 3.0.0-alpha.19 (2025-06-26) + +Full Changelog: [v3.0.0-alpha.2...v3.0.0-alpha.19](https://github.com/supermemoryai/python-sdk/compare/v3.0.0-alpha.2...v3.0.0-alpha.19) + +### Features + +* **api:** api update ([10e12a2](https://github.com/supermemoryai/python-sdk/commit/10e12a2f0453c3e5a2eaeee86a3da9738c161e50)) + ## 3.0.0-alpha.2 (2025-06-24) Full Changelog: [v3.0.0-alpha.1...v3.0.0-alpha.2](https://github.com/supermemoryai/python-sdk/compare/v3.0.0-alpha.1...v3.0.0-alpha.2) diff --git a/README.md b/README.md index 8557c60b..4458331d 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,23 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. +## File uploads + +Request parameters that correspond to file uploads can be passed as `bytes`, or a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`. + +```python +from pathlib import Path +from supermemory import Supermemory + +client = Supermemory() + +client.memories.upload_file( + file=Path("/path/to/file"), +) +``` + +The async client uses the exact same interface. If you pass a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance, the file contents will be read asynchronously automatically. + ## Handling errors When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `supermemory.APIConnectionError` is raised. diff --git a/api.md b/api.md index 77c6c399..fa4e72a2 100644 --- a/api.md +++ b/api.md @@ -3,7 +3,12 @@ Types: ```python -from supermemory.types import MemoryUpdateResponse, MemoryAddResponse, MemoryGetResponse +from supermemory.types import ( + MemoryUpdateResponse, + MemoryAddResponse, + MemoryGetResponse, + MemoryUploadFileResponse, +) ``` Methods: @@ -12,6 +17,7 @@ Methods: - client.memories.delete(id) -> None - client.memories.add(\*\*params) -> MemoryAddResponse - client.memories.get(id) -> MemoryGetResponse +- client.memories.upload_file(\*\*params) -> MemoryUploadFileResponse # Settings diff --git a/pyproject.toml b/pyproject.toml index 51b28cf1..40c7241e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "supermemory" -version = "3.0.0-alpha.2" +version = "3.0.0-alpha.19" description = "The official Python library for the supermemory API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/supermemory/_files.py b/src/supermemory/_files.py index 715cc207..0dcf63d3 100644 --- a/src/supermemory/_files.py +++ b/src/supermemory/_files.py @@ -34,7 +34,7 @@ def assert_is_file_content(obj: object, *, key: str | None = None) -> None: if not is_file_content(obj): prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`" raise RuntimeError( - f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead." + f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead. See https://github.com/supermemoryai/python-sdk/tree/main#file-uploads" ) from None diff --git a/src/supermemory/_version.py b/src/supermemory/_version.py index dfc97c46..d5e7dc08 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.0.0-alpha.2" # x-release-please-version +__version__ = "3.0.0-alpha.19" # x-release-please-version diff --git a/src/supermemory/resources/memories.py b/src/supermemory/resources/memories.py index 02cf1801..6fe1d56a 100644 --- a/src/supermemory/resources/memories.py +++ b/src/supermemory/resources/memories.py @@ -2,13 +2,13 @@ from __future__ import annotations -from typing import Dict, List, Union +from typing import Dict, List, Union, Mapping, cast import httpx -from ..types import memory_add_params, memory_update_params -from .._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven -from .._utils import maybe_transform, async_maybe_transform +from ..types import memory_add_params, memory_update_params, memory_upload_file_params +from .._types import NOT_GIVEN, Body, Query, Headers, NoneType, NotGiven, FileTypes +from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -21,6 +21,7 @@ from ..types.memory_add_response import MemoryAddResponse from ..types.memory_get_response import MemoryGetResponse from ..types.memory_update_response import MemoryUpdateResponse +from ..types.memory_upload_file_response import MemoryUploadFileResponse __all__ = ["MemoriesResource", "AsyncMemoriesResource"] @@ -243,6 +244,45 @@ def get( cast_to=MemoryGetResponse, ) + def upload_file( + self, + *, + file: FileTypes, + # 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, + ) -> MemoryUploadFileResponse: + """ + Upload a file to be processed + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal({"file": file}) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._post( + "/v3/memories/file", + body=maybe_transform(body, memory_upload_file_params.MemoryUploadFileParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MemoryUploadFileResponse, + ) + class AsyncMemoriesResource(AsyncAPIResource): @cached_property @@ -462,6 +502,45 @@ async def get( cast_to=MemoryGetResponse, ) + async def upload_file( + self, + *, + file: FileTypes, + # 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, + ) -> MemoryUploadFileResponse: + """ + Upload a file to be processed + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + body = deepcopy_minimal({"file": file}) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._post( + "/v3/memories/file", + body=await async_maybe_transform(body, memory_upload_file_params.MemoryUploadFileParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MemoryUploadFileResponse, + ) + class MemoriesResourceWithRawResponse: def __init__(self, memories: MemoriesResource) -> None: @@ -479,6 +558,9 @@ def __init__(self, memories: MemoriesResource) -> None: self.get = to_raw_response_wrapper( memories.get, ) + self.upload_file = to_raw_response_wrapper( + memories.upload_file, + ) class AsyncMemoriesResourceWithRawResponse: @@ -497,6 +579,9 @@ def __init__(self, memories: AsyncMemoriesResource) -> None: self.get = async_to_raw_response_wrapper( memories.get, ) + self.upload_file = async_to_raw_response_wrapper( + memories.upload_file, + ) class MemoriesResourceWithStreamingResponse: @@ -515,6 +600,9 @@ def __init__(self, memories: MemoriesResource) -> None: self.get = to_streamed_response_wrapper( memories.get, ) + self.upload_file = to_streamed_response_wrapper( + memories.upload_file, + ) class AsyncMemoriesResourceWithStreamingResponse: @@ -533,3 +621,6 @@ def __init__(self, memories: AsyncMemoriesResource) -> None: self.get = async_to_streamed_response_wrapper( memories.get, ) + self.upload_file = async_to_streamed_response_wrapper( + memories.upload_file, + ) diff --git a/src/supermemory/types/__init__.py b/src/supermemory/types/__init__.py index 6ca2ae1a..b87bc6d6 100644 --- a/src/supermemory/types/__init__.py +++ b/src/supermemory/types/__init__.py @@ -12,4 +12,6 @@ from .connection_get_response import ConnectionGetResponse as ConnectionGetResponse from .setting_update_response import SettingUpdateResponse as SettingUpdateResponse from .connection_create_params import ConnectionCreateParams as ConnectionCreateParams +from .memory_upload_file_params import MemoryUploadFileParams as MemoryUploadFileParams from .connection_create_response import ConnectionCreateResponse as ConnectionCreateResponse +from .memory_upload_file_response import MemoryUploadFileResponse as MemoryUploadFileResponse diff --git a/src/supermemory/types/memory_upload_file_params.py b/src/supermemory/types/memory_upload_file_params.py new file mode 100644 index 00000000..aa6c082a --- /dev/null +++ b/src/supermemory/types/memory_upload_file_params.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from .._types import FileTypes + +__all__ = ["MemoryUploadFileParams"] + + +class MemoryUploadFileParams(TypedDict, total=False): + file: Required[FileTypes] diff --git a/src/supermemory/types/memory_upload_file_response.py b/src/supermemory/types/memory_upload_file_response.py new file mode 100644 index 00000000..f67b958f --- /dev/null +++ b/src/supermemory/types/memory_upload_file_response.py @@ -0,0 +1,11 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["MemoryUploadFileResponse"] + + +class MemoryUploadFileResponse(BaseModel): + id: str + + status: str diff --git a/tests/api_resources/test_memories.py b/tests/api_resources/test_memories.py index c398ab8f..a9e1c726 100644 --- a/tests/api_resources/test_memories.py +++ b/tests/api_resources/test_memories.py @@ -13,6 +13,7 @@ MemoryAddResponse, MemoryGetResponse, MemoryUpdateResponse, + MemoryUploadFileResponse, ) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -222,6 +223,40 @@ def test_path_params_get(self, client: Supermemory) -> None: "", ) + @pytest.mark.skip() + @parametrize + def test_method_upload_file(self, client: Supermemory) -> None: + memory = client.memories.upload_file( + file=b"raw file contents", + ) + assert_matches_type(MemoryUploadFileResponse, memory, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_raw_response_upload_file(self, client: Supermemory) -> None: + response = client.memories.with_raw_response.upload_file( + file=b"raw file contents", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + memory = response.parse() + assert_matches_type(MemoryUploadFileResponse, memory, path=["response"]) + + @pytest.mark.skip() + @parametrize + def test_streaming_response_upload_file(self, client: Supermemory) -> None: + with client.memories.with_streaming_response.upload_file( + file=b"raw file contents", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + memory = response.parse() + assert_matches_type(MemoryUploadFileResponse, memory, path=["response"]) + + assert cast(Any, response.is_closed) is True + class TestAsyncMemories: parametrize = pytest.mark.parametrize( @@ -428,3 +463,37 @@ async def test_path_params_get(self, async_client: AsyncSupermemory) -> None: await async_client.memories.with_raw_response.get( "", ) + + @pytest.mark.skip() + @parametrize + async def test_method_upload_file(self, async_client: AsyncSupermemory) -> None: + memory = await async_client.memories.upload_file( + file=b"raw file contents", + ) + assert_matches_type(MemoryUploadFileResponse, memory, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_raw_response_upload_file(self, async_client: AsyncSupermemory) -> None: + response = await async_client.memories.with_raw_response.upload_file( + file=b"raw file contents", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + memory = await response.parse() + assert_matches_type(MemoryUploadFileResponse, memory, path=["response"]) + + @pytest.mark.skip() + @parametrize + async def test_streaming_response_upload_file(self, async_client: AsyncSupermemory) -> None: + async with async_client.memories.with_streaming_response.upload_file( + file=b"raw file contents", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + memory = await response.parse() + assert_matches_type(MemoryUploadFileResponse, memory, path=["response"]) + + assert cast(Any, response.is_closed) is True