From 05647b50fa712957585bf7bccb984816c4aa8a7a Mon Sep 17 00:00:00 2001 From: Dhravya Shah Date: Sun, 4 Oct 2026 22:12:40 -0700 Subject: [PATCH 1/4] feat!: generate the SDK from the v5 API with Fern, replacing Stainless Generate src/supermemory from https://api.supermemory.ai/v5/openapi using Fern's open-source Python generator run locally in Docker (the approach cloudflare/forge uses), so no hosted codegen service or plan is needed. - fern/: spec snapshot, overlay (method surface matching sdk-ts v5), schemas.yaml (named types: Document, Memory, SearchResult, Connector, ...) - custom/: hand-written 3.x-compatible client conventions (options, env vars, extra_headers/extra_query/extra_body/timeout, with_options, exception hierarchy, with_raw_response.parse, to_dict/to_json) plus two patches for bugs in Fern's generated core (recursive filter serialization, py3.9) - scripts/generate: fetch spec, name schemas, run Fern, layer custom/ - CI: py3.10-3.14 + pydantic v1 tests, lint, mypy, generated-code drift check, optional live-API tests; Generate SDK workflow opens a version-bumped PR when the spec changes; merging publishes to PyPI - tests: wire-level route/auth/body tests, compat tests, every README and MIGRATION snippet executed against a mock API - version 5.0.0rc1; requires Python 3.10+ BREAKING CHANGE: namespace-first v5 methods (client.add(namespace, ...), client.search(namespace, query=...)); container tags become namespaces. See MIGRATION.md. Co-Authored-By: Claude Opus 5.5 (1M context) --- .devcontainer/Dockerfile | 9 - .devcontainer/devcontainer.json | 43 - .github/workflows/ci.yml | 148 +- .github/workflows/generate.yml | 75 + .github/workflows/publish-pypi.yml | 55 +- .github/workflows/release-doctor.yml | 21 - .gitignore | 14 +- .python-version | 2 +- .release-please-manifest.json | 3 - .stats.yml | 4 - .vscode/settings.json | 3 - Brewfile | 2 - CHANGELOG.md | 19 + CONTRIBUTING.md | 121 - MIGRATION.md | 99 + README.md | 438 +- api.md | 113 - bin/check-release-environment | 21 - bin/publish-pypi | 6 - custom/__init__.tail.py | 14 + ...01-compat-response-and-model-helpers.patch | 93 + .../0002-resolve-recursive-forward-refs.patch | 62 + custom/supermemory/_compat.py | 541 ++ examples/.keep | 4 - examples/chat_with_memory.py | 32 + examples/errors_and_retries.py | 22 + examples/quickstart.py | 21 + fern/fern.config.json | 1 + fern/generators.yml | 38 + fern/openapi.json | 8200 +++++++++++++++++ fern/overlay.yaml | 43 + fern/schemas.yaml | 103 + noxfile.py | 9 - pyproject.toml | 252 +- reference.md | 2851 ++++++ release-please-config.json | 66 - requirements-dev.lock | 149 - requirements.lock | 76 - scripts/apply_custom.py | 103 + scripts/bootstrap | 27 - scripts/bump_version.py | 30 + scripts/format | 8 - scripts/generate | 48 + scripts/lint | 16 - scripts/name_schemas.py | 176 + scripts/test | 15 - scripts/utils/ruffen-docs.py | 167 - scripts/utils/upload-artifact.sh | 27 - src/supermemory/__init__.py | 780 +- src/supermemory/_base_client.py | 2131 ----- src/supermemory/_client.py | 994 -- src/supermemory/_compat.py | 647 +- src/supermemory/_constants.py | 14 - src/supermemory/_default_clients.py | 30 + src/supermemory/_exceptions.py | 108 - src/supermemory/_files.py | 173 - src/supermemory/_models.py | 952 -- src/supermemory/_qs.py | 149 - src/supermemory/_resource.py | 43 - src/supermemory/_response.py | 835 -- src/supermemory/_streaming.py | 338 - src/supermemory/_types.py | 273 - src/supermemory/_utils/__init__.py | 64 - src/supermemory/_utils/_compat.py | 45 - src/supermemory/_utils/_datetime_parse.py | 136 - src/supermemory/_utils/_json.py | 35 - src/supermemory/_utils/_logs.py | 25 - src/supermemory/_utils/_path.py | 127 - src/supermemory/_utils/_proxy.py | 65 - src/supermemory/_utils/_reflection.py | 42 - src/supermemory/_utils/_resources_proxy.py | 24 - src/supermemory/_utils/_streams.py | 12 - src/supermemory/_utils/_sync.py | 58 - src/supermemory/_utils/_transform.py | 457 - src/supermemory/_utils/_typing.py | 156 - src/supermemory/_utils/_utils.py | 433 - src/supermemory/_version.py | 4 - src/supermemory/client.py | 998 ++ src/supermemory/connectors/__init__.py | 34 + src/supermemory/connectors/client.py | 813 ++ src/supermemory/connectors/raw_client.py | 1748 ++++ src/supermemory/connectors/types/__init__.py | 36 + ...ete_connectors_request_delete_documents.py | 5 + src/supermemory/core/__init__.py | 134 + src/supermemory/core/api_error.py | 23 + src/supermemory/core/client_wrapper.py | 145 + src/supermemory/core/datetime_utils.py | 70 + src/supermemory/core/file.py | 67 + src/supermemory/core/force_multipart.py | 18 + src/supermemory/core/http_client.py | 851 ++ src/supermemory/core/http_response.py | 72 + src/supermemory/core/http_sse/__init__.py | 42 + src/supermemory/core/http_sse/_api.py | 455 + src/supermemory/core/http_sse/_decoders.py | 74 + src/supermemory/core/http_sse/_exceptions.py | 7 + src/supermemory/core/http_sse/_models.py | 17 + src/supermemory/core/jsonable_encoder.py | 120 + src/supermemory/core/logging.py | 107 + src/supermemory/core/parse_error.py | 36 + src/supermemory/core/pydantic_utilities.py | 508 + src/supermemory/core/query_encoder.py | 58 + src/supermemory/core/remove_none_from_dict.py | 11 + src/supermemory/core/request_options.py | 40 + src/supermemory/core/serialization.py | 383 + src/supermemory/core/unchecked_base_model.py | 539 ++ src/supermemory/documents/__init__.py | 4 + src/supermemory/documents/client.py | 1138 +++ src/supermemory/documents/raw_client.py | 2182 +++++ src/supermemory/environment.py | 7 + src/supermemory/errors/__init__.py | 59 + src/supermemory/errors/bad_request_error.py | 11 + src/supermemory/errors/conflict_error.py | 11 + src/supermemory/errors/forbidden_error.py | 11 + .../errors/internal_server_error.py | 11 + src/supermemory/errors/not_found_error.py | 11 + .../errors/payment_required_error.py | 11 + .../errors/service_unavailable_error.py | 11 + src/supermemory/errors/unauthorized_error.py | 11 + src/supermemory/lib/.keep | 4 - src/supermemory/memories/__init__.py | 4 + src/supermemory/memories/client.py | 255 + src/supermemory/memories/raw_client.py | 465 + src/supermemory/namespaces/__init__.py | 4 + src/supermemory/namespaces/client.py | 407 + src/supermemory/namespaces/raw_client.py | 893 ++ src/supermemory/organization/__init__.py | 4 + src/supermemory/organization/client.py | 209 + src/supermemory/organization/raw_client.py | 394 + src/supermemory/profiles/__init__.py | 4 + src/supermemory/profiles/client.py | 329 + src/supermemory/profiles/raw_client.py | 711 ++ src/supermemory/raw_client.py | 1201 +++ src/supermemory/requests/__init__.py | 307 + .../requests/bad_request_error_body.py | 8 + .../requests/batch_add_item_result.py | 30 + src/supermemory/requests/batch_add_result.py | 23 + src/supermemory/requests/chunk.py | 42 + src/supermemory/requests/chunk_list_item.py | 44 + src/supermemory/requests/chunk_system.py | 17 + src/supermemory/requests/connector.py | 84 + .../requests/connector_capabilities.py | 15 + .../requests/connector_config_value.py | 5 + src/supermemory/requests/connector_deleted.py | 10 + .../requests/connector_last_sync.py | 21 + src/supermemory/requests/connector_list.py | 15 + src/supermemory/requests/connector_picker.py | 15 + .../requests/connector_selection.py | 31 + .../requests/connector_selection_item.py | 8 + src/supermemory/requests/connector_setup.py | 89 + .../requests/connector_setup_github.py | 18 + .../requests/connector_setup_gmail.py | 18 + .../requests/connector_setup_google_drive.py | 21 + .../requests/connector_setup_granola.py | 19 + .../requests/connector_setup_notion.py | 18 + .../requests/connector_setup_onedrive.py | 18 + .../requests/connector_setup_result.py | 26 + .../requests/connector_setup_s3.py | 19 + .../requests/connector_setup_web_crawler.py | 19 + src/supermemory/requests/connector_sync.py | 31 + .../requests/connector_sync_failure.py | 14 + .../requests/connector_sync_started.py | 14 + .../requests/delete_document_error.py | 15 + .../requests/delete_documents_result.py | 19 + src/supermemory/requests/document.py | 59 + src/supermemory/requests/document_input.py | 41 + .../requests/document_list_item.py | 47 + src/supermemory/requests/document_ref.py | 15 + src/supermemory/requests/document_system.py | 27 + src/supermemory/requests/error_response.py | 15 + .../requests/file_upload_result.py | 17 + src/supermemory/requests/filter_and.py | 22 + .../requests/filter_array_contains.py | 21 + src/supermemory/requests/filter_expression.py | 12 + .../requests/filter_number_compare.py | 21 + src/supermemory/requests/filter_or.py | 22 + src/supermemory/requests/filter_predicate.py | 17 + .../requests/filter_string_contains.py | 29 + .../requests/filter_string_equals.py | 29 + .../requests/filter_value_equals.py | 22 + .../requests/filter_value_equals_value.py | 5 + src/supermemory/requests/forget_error.py | 15 + src/supermemory/requests/forget_match.py | 15 + src/supermemory/requests/forget_result.py | 24 + .../requests/google_drive_config.py | 14 + src/supermemory/requests/granola_config.py | 12 + .../requests/list_request_limit.py | 5 + src/supermemory/requests/list_request_page.py | 5 + src/supermemory/requests/list_response.py | 35 + src/supermemory/requests/memory.py | 58 + src/supermemory/requests/memory_system.py | 22 + src/supermemory/requests/metadata_value.py | 5 + src/supermemory/requests/namespace.py | 39 + src/supermemory/requests/namespace_deleted.py | 28 + src/supermemory/requests/namespace_details.py | 24 + src/supermemory/requests/namespace_system.py | 22 + src/supermemory/requests/organization.py | 20 + src/supermemory/requests/pagination.py | 30 + src/supermemory/requests/profile.py | 27 + src/supermemory/requests/profile_buckets.py | 12 + src/supermemory/requests/profile_fact.py | 15 + src/supermemory/requests/profile_response.py | 11 + src/supermemory/requests/related_memories.py | 27 + src/supermemory/requests/related_memory.py | 34 + .../requests/related_memory_system.py | 15 + src/supermemory/requests/s3config.py | 24 + .../requests/search_request_attach.py | 24 + src/supermemory/requests/search_response.py | 19 + src/supermemory/requests/search_result.py | 50 + .../requests/search_result_included.py | 21 + .../search_result_included_document.py | 47 + .../requests/search_result_system.py | 20 + .../requests/validation_error_response.py | 23 + .../validation_error_response_error_item.py | 11 + ...ion_error_response_error_item_path_item.py | 5 + .../requests/web_crawler_config.py | 16 + src/supermemory/resources/__init__.py | 75 - src/supermemory/resources/connections.py | 1028 --- src/supermemory/resources/documents.py | 1510 --- src/supermemory/resources/memories.py | 374 - src/supermemory/resources/search.py | 739 -- src/supermemory/resources/settings.py | 302 - src/supermemory/types/__init__.py | 427 +- src/supermemory/types/add_response.py | 13 - .../types/bad_request_error_body.py | 8 + .../types/batch_add_item_result.py | 43 + src/supermemory/types/batch_add_result.py | 34 + src/supermemory/types/chunk.py | 53 + src/supermemory/types/chunk_list_item.py | 60 + src/supermemory/types/chunk_system.py | 34 + src/supermemory/types/client_add_params.py | 80 - .../types/client_profile_params.py | 140 - .../types/connection_configure_params.py | 12 - .../types/connection_configure_response.py | 17 - .../types/connection_create_params.py | 23 - .../types/connection_create_response.py | 19 - .../types/connection_delete_by_id_params.py | 14 - .../types/connection_delete_by_id_response.py | 17 - .../connection_delete_by_provider_params.py | 15 - .../connection_delete_by_provider_response.py | 17 - .../types/connection_get_by_id_response.py | 44 - .../types/connection_get_by_tag_params.py | 15 - .../types/connection_get_by_tag_response.py | 44 - .../types/connection_import_params.py | 15 - .../types/connection_import_response.py | 7 - .../types/connection_list_documents_params.py | 15 - .../connection_list_documents_response.py | 29 - .../types/connection_list_params.py | 15 - .../types/connection_list_response.py | 47 - .../types/connection_resources_params.py | 15 - .../types/connection_resources_response.py | 13 - src/supermemory/types/connector.py | 121 + .../types/connector_capabilities.py | 28 + .../types/connector_config_value.py | 5 + src/supermemory/types/connector_deleted.py | 21 + src/supermemory/types/connector_error_code.py | 7 + src/supermemory/types/connector_last_sync.py | 39 + src/supermemory/types/connector_list.py | 26 + src/supermemory/types/connector_picker.py | 30 + src/supermemory/types/connector_provider.py | 7 + src/supermemory/types/connector_selection.py | 42 + .../types/connector_selection_item.py | 21 + src/supermemory/types/connector_setup.py | 177 + .../types/connector_setup_github.py | 38 + .../types/connector_setup_gmail.py | 38 + .../types/connector_setup_google_drive.py | 41 + .../types/connector_setup_granola.py | 35 + .../types/connector_setup_notion.py | 38 + .../types/connector_setup_onedrive.py | 38 + .../types/connector_setup_result.py | 47 + src/supermemory/types/connector_setup_s3.py | 35 + .../types/connector_setup_web_crawler.py | 35 + src/supermemory/types/connector_status.py | 5 + src/supermemory/types/connector_sync.py | 53 + .../types/connector_sync_failure.py | 28 + .../types/connector_sync_started.py | 25 + .../types/connector_sync_status.py | 5 + .../types/connector_sync_trigger.py | 5 + .../types/delete_document_error.py | 28 + .../types/delete_documents_result.py | 35 + src/supermemory/types/document.py | 70 + src/supermemory/types/document_add_params.py | 80 - .../types/document_add_response.py | 13 - src/supermemory/types/document_attachment.py | 5 + .../types/document_batch_add_params.py | 178 - .../types/document_batch_add_response.py | 40 - .../types/document_delete_bulk_params.py | 24 - .../types/document_delete_bulk_response.py | 37 - .../types/document_get_response.py | 153 - src/supermemory/types/document_input.py | 58 + src/supermemory/types/document_list_item.py | 58 + src/supermemory/types/document_list_params.py | 139 - .../types/document_list_processing_params.py | 28 - .../document_list_processing_response.py | 110 - .../types/document_list_response.py | 106 - src/supermemory/types/document_ref.py | 28 + src/supermemory/types/document_system.py | 48 + .../types/document_update_params.py | 87 - .../types/document_update_response.py | 13 - .../types/document_upload_file_params.py | 95 - .../types/document_upload_file_response.py | 13 - src/supermemory/types/dreaming_mode.py | 5 + src/supermemory/types/error_response.py | 28 + src/supermemory/types/file_type.py | 5 + src/supermemory/types/file_upload_result.py | 28 + src/supermemory/types/filter_and.py | 36 + .../types/filter_array_contains.py | 34 + .../types/filter_array_contains_operator.py | 5 + src/supermemory/types/filter_expression.py | 12 + .../types/filter_number_compare.py | 34 + .../types/filter_number_compare_operator.py | 5 + src/supermemory/types/filter_or.py | 36 + src/supermemory/types/filter_predicate.py | 13 + .../types/filter_string_contains.py | 45 + .../types/filter_string_contains_operator.py | 5 + src/supermemory/types/filter_string_equals.py | 45 + .../types/filter_string_equals_operator.py | 5 + src/supermemory/types/filter_value_equals.py | 35 + .../types/filter_value_equals_operator.py | 5 + .../types/filter_value_equals_value.py | 5 + src/supermemory/types/forget_error.py | 28 + src/supermemory/types/forget_match.py | 28 + src/supermemory/types/forget_result.py | 35 + src/supermemory/types/google_drive_config.py | 33 + .../types/google_drive_config_sync_scope.py | 5 + src/supermemory/types/granola_config.py | 26 + src/supermemory/types/list_request_limit.py | 5 + src/supermemory/types/list_request_page.py | 5 + src/supermemory/types/list_response.py | 46 + src/supermemory/types/list_sort.py | 5 + src/supermemory/types/list_type.py | 5 + src/supermemory/types/memory.py | 88 + src/supermemory/types/memory_forget_params.py | 26 - .../types/memory_forget_response.py | 15 - src/supermemory/types/memory_relation.py | 5 + src/supermemory/types/memory_system.py | 43 + .../types/memory_update_memory_params.py | 64 - .../types/memory_update_memory_response.py | 43 - src/supermemory/types/metadata_value.py | 5 + src/supermemory/types/namespace.py | 59 + src/supermemory/types/namespace_deleted.py | 48 + src/supermemory/types/namespace_details.py | 43 + src/supermemory/types/namespace_system.py | 43 + src/supermemory/types/organization.py | 41 + src/supermemory/types/pagination.py | 56 + src/supermemory/types/profile.py | 38 + src/supermemory/types/profile_buckets.py | 23 + src/supermemory/types/profile_fact.py | 28 + src/supermemory/types/profile_response.py | 208 +- src/supermemory/types/related_memories.py | 38 + src/supermemory/types/related_memory.py | 45 + .../types/related_memory_system.py | 33 + src/supermemory/types/s3config.py | 42 + .../types/search_documents_params.py | 183 - .../types/search_documents_response.py | 69 - .../types/search_execute_params.py | 183 - .../types/search_execute_response.py | 69 - .../types/search_memories_params.py | 191 - .../types/search_memories_response.py | 186 - src/supermemory/types/search_mode.py | 5 + .../types/search_request_attach.py | 37 + .../types/search_request_rerank.py | 5 + src/supermemory/types/search_response.py | 35 + src/supermemory/types/search_result.py | 69 + .../types/search_result_included.py | 34 + .../types/search_result_included_document.py | 67 + src/supermemory/types/search_result_system.py | 42 + src/supermemory/types/setting_get_response.py | 62 - .../types/setting_update_params.py | 63 - .../types/setting_update_response.py | 70 - src/supermemory/types/sort_order.py | 5 + src/supermemory/types/task_type.py | 5 + .../types/validation_error_response.py | 34 + .../validation_error_response_error_item.py | 22 + ...ion_error_response_error_item_path_item.py | 5 + src/supermemory/types/web_crawler_config.py | 34 + src/supermemory_new/lib/.keep | 4 - tests/__init__.py | 1 - tests/api_resources/__init__.py | 1 - tests/api_resources/test_client.py | 244 - tests/api_resources/test_connections.py | 904 -- tests/api_resources/test_documents.py | 1001 -- tests/api_resources/test_memories.py | 227 - tests/api_resources/test_search.py | 436 - tests/api_resources/test_settings.py | 196 - tests/conftest.py | 84 - tests/live/test_live.py | 84 + tests/sample_file.txt | 1 - tests/test_client.py | 2074 +---- tests/test_compat.py | 193 + tests/test_docs.py | 86 + tests/test_extract_files.py | 91 - tests/test_files.py | 148 - tests/test_models.py | 1017 -- tests/test_qs.py | 78 - tests/test_required_args.py | 111 - tests/test_response.py | 277 - tests/test_streaming.py | 252 - tests/test_transform.py | 460 - tests/test_utils/test_datetime_parse.py | 110 - tests/test_utils/test_json.py | 126 - tests/test_utils/test_path.py | 89 - tests/test_utils/test_proxy.py | 34 - tests/test_utils/test_typing.py | 73 - tests/utils.py | 167 - uv.lock | 1522 +++ 405 files changed, 37626 insertions(+), 24871 deletions(-) delete mode 100644 .devcontainer/Dockerfile delete mode 100644 .devcontainer/devcontainer.json create mode 100644 .github/workflows/generate.yml delete mode 100644 .github/workflows/release-doctor.yml delete mode 100644 .release-please-manifest.json delete mode 100644 .stats.yml delete mode 100644 .vscode/settings.json delete mode 100644 Brewfile delete mode 100644 CONTRIBUTING.md create mode 100644 MIGRATION.md delete mode 100644 api.md delete mode 100644 bin/check-release-environment delete mode 100644 bin/publish-pypi create mode 100644 custom/__init__.tail.py create mode 100644 custom/patches/0001-compat-response-and-model-helpers.patch create mode 100644 custom/patches/0002-resolve-recursive-forward-refs.patch create mode 100644 custom/supermemory/_compat.py delete mode 100644 examples/.keep create mode 100644 examples/chat_with_memory.py create mode 100644 examples/errors_and_retries.py create mode 100644 examples/quickstart.py create mode 100644 fern/fern.config.json create mode 100644 fern/generators.yml create mode 100644 fern/openapi.json create mode 100644 fern/overlay.yaml create mode 100644 fern/schemas.yaml delete mode 100644 noxfile.py create mode 100644 reference.md delete mode 100644 release-please-config.json delete mode 100644 requirements-dev.lock delete mode 100644 requirements.lock create mode 100644 scripts/apply_custom.py delete mode 100755 scripts/bootstrap create mode 100644 scripts/bump_version.py delete mode 100755 scripts/format create mode 100755 scripts/generate delete mode 100755 scripts/lint create mode 100644 scripts/name_schemas.py delete mode 100755 scripts/test delete mode 100644 scripts/utils/ruffen-docs.py delete mode 100755 scripts/utils/upload-artifact.sh delete mode 100644 src/supermemory/_base_client.py delete mode 100644 src/supermemory/_client.py delete mode 100644 src/supermemory/_constants.py create mode 100644 src/supermemory/_default_clients.py delete mode 100644 src/supermemory/_exceptions.py delete mode 100644 src/supermemory/_files.py delete mode 100644 src/supermemory/_models.py delete mode 100644 src/supermemory/_qs.py delete mode 100644 src/supermemory/_resource.py delete mode 100644 src/supermemory/_response.py delete mode 100644 src/supermemory/_streaming.py delete mode 100644 src/supermemory/_types.py delete mode 100644 src/supermemory/_utils/__init__.py delete mode 100644 src/supermemory/_utils/_compat.py delete mode 100644 src/supermemory/_utils/_datetime_parse.py delete mode 100644 src/supermemory/_utils/_json.py delete mode 100644 src/supermemory/_utils/_logs.py delete mode 100644 src/supermemory/_utils/_path.py delete mode 100644 src/supermemory/_utils/_proxy.py delete mode 100644 src/supermemory/_utils/_reflection.py delete mode 100644 src/supermemory/_utils/_resources_proxy.py delete mode 100644 src/supermemory/_utils/_streams.py delete mode 100644 src/supermemory/_utils/_sync.py delete mode 100644 src/supermemory/_utils/_transform.py delete mode 100644 src/supermemory/_utils/_typing.py delete mode 100644 src/supermemory/_utils/_utils.py delete mode 100644 src/supermemory/_version.py create mode 100644 src/supermemory/client.py create mode 100644 src/supermemory/connectors/__init__.py create mode 100644 src/supermemory/connectors/client.py create mode 100644 src/supermemory/connectors/raw_client.py create mode 100644 src/supermemory/connectors/types/__init__.py create mode 100644 src/supermemory/connectors/types/delete_connectors_request_delete_documents.py create mode 100644 src/supermemory/core/__init__.py create mode 100644 src/supermemory/core/api_error.py create mode 100644 src/supermemory/core/client_wrapper.py create mode 100644 src/supermemory/core/datetime_utils.py create mode 100644 src/supermemory/core/file.py create mode 100644 src/supermemory/core/force_multipart.py create mode 100644 src/supermemory/core/http_client.py create mode 100644 src/supermemory/core/http_response.py create mode 100644 src/supermemory/core/http_sse/__init__.py create mode 100644 src/supermemory/core/http_sse/_api.py create mode 100644 src/supermemory/core/http_sse/_decoders.py create mode 100644 src/supermemory/core/http_sse/_exceptions.py create mode 100644 src/supermemory/core/http_sse/_models.py create mode 100644 src/supermemory/core/jsonable_encoder.py create mode 100644 src/supermemory/core/logging.py create mode 100644 src/supermemory/core/parse_error.py create mode 100644 src/supermemory/core/pydantic_utilities.py create mode 100644 src/supermemory/core/query_encoder.py create mode 100644 src/supermemory/core/remove_none_from_dict.py create mode 100644 src/supermemory/core/request_options.py create mode 100644 src/supermemory/core/serialization.py create mode 100644 src/supermemory/core/unchecked_base_model.py create mode 100644 src/supermemory/documents/__init__.py create mode 100644 src/supermemory/documents/client.py create mode 100644 src/supermemory/documents/raw_client.py create mode 100644 src/supermemory/environment.py create mode 100644 src/supermemory/errors/__init__.py create mode 100644 src/supermemory/errors/bad_request_error.py create mode 100644 src/supermemory/errors/conflict_error.py create mode 100644 src/supermemory/errors/forbidden_error.py create mode 100644 src/supermemory/errors/internal_server_error.py create mode 100644 src/supermemory/errors/not_found_error.py create mode 100644 src/supermemory/errors/payment_required_error.py create mode 100644 src/supermemory/errors/service_unavailable_error.py create mode 100644 src/supermemory/errors/unauthorized_error.py delete mode 100644 src/supermemory/lib/.keep create mode 100644 src/supermemory/memories/__init__.py create mode 100644 src/supermemory/memories/client.py create mode 100644 src/supermemory/memories/raw_client.py create mode 100644 src/supermemory/namespaces/__init__.py create mode 100644 src/supermemory/namespaces/client.py create mode 100644 src/supermemory/namespaces/raw_client.py create mode 100644 src/supermemory/organization/__init__.py create mode 100644 src/supermemory/organization/client.py create mode 100644 src/supermemory/organization/raw_client.py create mode 100644 src/supermemory/profiles/__init__.py create mode 100644 src/supermemory/profiles/client.py create mode 100644 src/supermemory/profiles/raw_client.py create mode 100644 src/supermemory/raw_client.py create mode 100644 src/supermemory/requests/__init__.py create mode 100644 src/supermemory/requests/bad_request_error_body.py create mode 100644 src/supermemory/requests/batch_add_item_result.py create mode 100644 src/supermemory/requests/batch_add_result.py create mode 100644 src/supermemory/requests/chunk.py create mode 100644 src/supermemory/requests/chunk_list_item.py create mode 100644 src/supermemory/requests/chunk_system.py create mode 100644 src/supermemory/requests/connector.py create mode 100644 src/supermemory/requests/connector_capabilities.py create mode 100644 src/supermemory/requests/connector_config_value.py create mode 100644 src/supermemory/requests/connector_deleted.py create mode 100644 src/supermemory/requests/connector_last_sync.py create mode 100644 src/supermemory/requests/connector_list.py create mode 100644 src/supermemory/requests/connector_picker.py create mode 100644 src/supermemory/requests/connector_selection.py create mode 100644 src/supermemory/requests/connector_selection_item.py create mode 100644 src/supermemory/requests/connector_setup.py create mode 100644 src/supermemory/requests/connector_setup_github.py create mode 100644 src/supermemory/requests/connector_setup_gmail.py create mode 100644 src/supermemory/requests/connector_setup_google_drive.py create mode 100644 src/supermemory/requests/connector_setup_granola.py create mode 100644 src/supermemory/requests/connector_setup_notion.py create mode 100644 src/supermemory/requests/connector_setup_onedrive.py create mode 100644 src/supermemory/requests/connector_setup_result.py create mode 100644 src/supermemory/requests/connector_setup_s3.py create mode 100644 src/supermemory/requests/connector_setup_web_crawler.py create mode 100644 src/supermemory/requests/connector_sync.py create mode 100644 src/supermemory/requests/connector_sync_failure.py create mode 100644 src/supermemory/requests/connector_sync_started.py create mode 100644 src/supermemory/requests/delete_document_error.py create mode 100644 src/supermemory/requests/delete_documents_result.py create mode 100644 src/supermemory/requests/document.py create mode 100644 src/supermemory/requests/document_input.py create mode 100644 src/supermemory/requests/document_list_item.py create mode 100644 src/supermemory/requests/document_ref.py create mode 100644 src/supermemory/requests/document_system.py create mode 100644 src/supermemory/requests/error_response.py create mode 100644 src/supermemory/requests/file_upload_result.py create mode 100644 src/supermemory/requests/filter_and.py create mode 100644 src/supermemory/requests/filter_array_contains.py create mode 100644 src/supermemory/requests/filter_expression.py create mode 100644 src/supermemory/requests/filter_number_compare.py create mode 100644 src/supermemory/requests/filter_or.py create mode 100644 src/supermemory/requests/filter_predicate.py create mode 100644 src/supermemory/requests/filter_string_contains.py create mode 100644 src/supermemory/requests/filter_string_equals.py create mode 100644 src/supermemory/requests/filter_value_equals.py create mode 100644 src/supermemory/requests/filter_value_equals_value.py create mode 100644 src/supermemory/requests/forget_error.py create mode 100644 src/supermemory/requests/forget_match.py create mode 100644 src/supermemory/requests/forget_result.py create mode 100644 src/supermemory/requests/google_drive_config.py create mode 100644 src/supermemory/requests/granola_config.py create mode 100644 src/supermemory/requests/list_request_limit.py create mode 100644 src/supermemory/requests/list_request_page.py create mode 100644 src/supermemory/requests/list_response.py create mode 100644 src/supermemory/requests/memory.py create mode 100644 src/supermemory/requests/memory_system.py create mode 100644 src/supermemory/requests/metadata_value.py create mode 100644 src/supermemory/requests/namespace.py create mode 100644 src/supermemory/requests/namespace_deleted.py create mode 100644 src/supermemory/requests/namespace_details.py create mode 100644 src/supermemory/requests/namespace_system.py create mode 100644 src/supermemory/requests/organization.py create mode 100644 src/supermemory/requests/pagination.py create mode 100644 src/supermemory/requests/profile.py create mode 100644 src/supermemory/requests/profile_buckets.py create mode 100644 src/supermemory/requests/profile_fact.py create mode 100644 src/supermemory/requests/profile_response.py create mode 100644 src/supermemory/requests/related_memories.py create mode 100644 src/supermemory/requests/related_memory.py create mode 100644 src/supermemory/requests/related_memory_system.py create mode 100644 src/supermemory/requests/s3config.py create mode 100644 src/supermemory/requests/search_request_attach.py create mode 100644 src/supermemory/requests/search_response.py create mode 100644 src/supermemory/requests/search_result.py create mode 100644 src/supermemory/requests/search_result_included.py create mode 100644 src/supermemory/requests/search_result_included_document.py create mode 100644 src/supermemory/requests/search_result_system.py create mode 100644 src/supermemory/requests/validation_error_response.py create mode 100644 src/supermemory/requests/validation_error_response_error_item.py create mode 100644 src/supermemory/requests/validation_error_response_error_item_path_item.py create mode 100644 src/supermemory/requests/web_crawler_config.py delete mode 100644 src/supermemory/resources/__init__.py delete mode 100644 src/supermemory/resources/connections.py delete mode 100644 src/supermemory/resources/documents.py delete mode 100644 src/supermemory/resources/memories.py delete mode 100644 src/supermemory/resources/search.py delete mode 100644 src/supermemory/resources/settings.py delete mode 100644 src/supermemory/types/add_response.py create mode 100644 src/supermemory/types/bad_request_error_body.py create mode 100644 src/supermemory/types/batch_add_item_result.py create mode 100644 src/supermemory/types/batch_add_result.py create mode 100644 src/supermemory/types/chunk.py create mode 100644 src/supermemory/types/chunk_list_item.py create mode 100644 src/supermemory/types/chunk_system.py delete mode 100644 src/supermemory/types/client_add_params.py delete mode 100644 src/supermemory/types/client_profile_params.py delete mode 100644 src/supermemory/types/connection_configure_params.py delete mode 100644 src/supermemory/types/connection_configure_response.py delete mode 100644 src/supermemory/types/connection_create_params.py delete mode 100644 src/supermemory/types/connection_create_response.py delete mode 100644 src/supermemory/types/connection_delete_by_id_params.py delete mode 100644 src/supermemory/types/connection_delete_by_id_response.py delete mode 100644 src/supermemory/types/connection_delete_by_provider_params.py delete mode 100644 src/supermemory/types/connection_delete_by_provider_response.py delete mode 100644 src/supermemory/types/connection_get_by_id_response.py delete mode 100644 src/supermemory/types/connection_get_by_tag_params.py delete mode 100644 src/supermemory/types/connection_get_by_tag_response.py delete mode 100644 src/supermemory/types/connection_import_params.py delete mode 100644 src/supermemory/types/connection_import_response.py delete mode 100644 src/supermemory/types/connection_list_documents_params.py delete mode 100644 src/supermemory/types/connection_list_documents_response.py delete mode 100644 src/supermemory/types/connection_list_params.py delete mode 100644 src/supermemory/types/connection_list_response.py delete mode 100644 src/supermemory/types/connection_resources_params.py delete mode 100644 src/supermemory/types/connection_resources_response.py create mode 100644 src/supermemory/types/connector.py create mode 100644 src/supermemory/types/connector_capabilities.py create mode 100644 src/supermemory/types/connector_config_value.py create mode 100644 src/supermemory/types/connector_deleted.py create mode 100644 src/supermemory/types/connector_error_code.py create mode 100644 src/supermemory/types/connector_last_sync.py create mode 100644 src/supermemory/types/connector_list.py create mode 100644 src/supermemory/types/connector_picker.py create mode 100644 src/supermemory/types/connector_provider.py create mode 100644 src/supermemory/types/connector_selection.py create mode 100644 src/supermemory/types/connector_selection_item.py create mode 100644 src/supermemory/types/connector_setup.py create mode 100644 src/supermemory/types/connector_setup_github.py create mode 100644 src/supermemory/types/connector_setup_gmail.py create mode 100644 src/supermemory/types/connector_setup_google_drive.py create mode 100644 src/supermemory/types/connector_setup_granola.py create mode 100644 src/supermemory/types/connector_setup_notion.py create mode 100644 src/supermemory/types/connector_setup_onedrive.py create mode 100644 src/supermemory/types/connector_setup_result.py create mode 100644 src/supermemory/types/connector_setup_s3.py create mode 100644 src/supermemory/types/connector_setup_web_crawler.py create mode 100644 src/supermemory/types/connector_status.py create mode 100644 src/supermemory/types/connector_sync.py create mode 100644 src/supermemory/types/connector_sync_failure.py create mode 100644 src/supermemory/types/connector_sync_started.py create mode 100644 src/supermemory/types/connector_sync_status.py create mode 100644 src/supermemory/types/connector_sync_trigger.py create mode 100644 src/supermemory/types/delete_document_error.py create mode 100644 src/supermemory/types/delete_documents_result.py create mode 100644 src/supermemory/types/document.py delete mode 100644 src/supermemory/types/document_add_params.py delete mode 100644 src/supermemory/types/document_add_response.py create mode 100644 src/supermemory/types/document_attachment.py delete mode 100644 src/supermemory/types/document_batch_add_params.py delete mode 100644 src/supermemory/types/document_batch_add_response.py delete mode 100644 src/supermemory/types/document_delete_bulk_params.py delete mode 100644 src/supermemory/types/document_delete_bulk_response.py delete mode 100644 src/supermemory/types/document_get_response.py create mode 100644 src/supermemory/types/document_input.py create mode 100644 src/supermemory/types/document_list_item.py delete mode 100644 src/supermemory/types/document_list_params.py delete mode 100644 src/supermemory/types/document_list_processing_params.py delete mode 100644 src/supermemory/types/document_list_processing_response.py delete mode 100644 src/supermemory/types/document_list_response.py create mode 100644 src/supermemory/types/document_ref.py create mode 100644 src/supermemory/types/document_system.py delete mode 100644 src/supermemory/types/document_update_params.py delete mode 100644 src/supermemory/types/document_update_response.py delete mode 100644 src/supermemory/types/document_upload_file_params.py delete mode 100644 src/supermemory/types/document_upload_file_response.py create mode 100644 src/supermemory/types/dreaming_mode.py create mode 100644 src/supermemory/types/error_response.py create mode 100644 src/supermemory/types/file_type.py create mode 100644 src/supermemory/types/file_upload_result.py create mode 100644 src/supermemory/types/filter_and.py create mode 100644 src/supermemory/types/filter_array_contains.py create mode 100644 src/supermemory/types/filter_array_contains_operator.py create mode 100644 src/supermemory/types/filter_expression.py create mode 100644 src/supermemory/types/filter_number_compare.py create mode 100644 src/supermemory/types/filter_number_compare_operator.py create mode 100644 src/supermemory/types/filter_or.py create mode 100644 src/supermemory/types/filter_predicate.py create mode 100644 src/supermemory/types/filter_string_contains.py create mode 100644 src/supermemory/types/filter_string_contains_operator.py create mode 100644 src/supermemory/types/filter_string_equals.py create mode 100644 src/supermemory/types/filter_string_equals_operator.py create mode 100644 src/supermemory/types/filter_value_equals.py create mode 100644 src/supermemory/types/filter_value_equals_operator.py create mode 100644 src/supermemory/types/filter_value_equals_value.py create mode 100644 src/supermemory/types/forget_error.py create mode 100644 src/supermemory/types/forget_match.py create mode 100644 src/supermemory/types/forget_result.py create mode 100644 src/supermemory/types/google_drive_config.py create mode 100644 src/supermemory/types/google_drive_config_sync_scope.py create mode 100644 src/supermemory/types/granola_config.py create mode 100644 src/supermemory/types/list_request_limit.py create mode 100644 src/supermemory/types/list_request_page.py create mode 100644 src/supermemory/types/list_response.py create mode 100644 src/supermemory/types/list_sort.py create mode 100644 src/supermemory/types/list_type.py create mode 100644 src/supermemory/types/memory.py delete mode 100644 src/supermemory/types/memory_forget_params.py delete mode 100644 src/supermemory/types/memory_forget_response.py create mode 100644 src/supermemory/types/memory_relation.py create mode 100644 src/supermemory/types/memory_system.py delete mode 100644 src/supermemory/types/memory_update_memory_params.py delete mode 100644 src/supermemory/types/memory_update_memory_response.py create mode 100644 src/supermemory/types/metadata_value.py create mode 100644 src/supermemory/types/namespace.py create mode 100644 src/supermemory/types/namespace_deleted.py create mode 100644 src/supermemory/types/namespace_details.py create mode 100644 src/supermemory/types/namespace_system.py create mode 100644 src/supermemory/types/organization.py create mode 100644 src/supermemory/types/pagination.py create mode 100644 src/supermemory/types/profile.py create mode 100644 src/supermemory/types/profile_buckets.py create mode 100644 src/supermemory/types/profile_fact.py create mode 100644 src/supermemory/types/related_memories.py create mode 100644 src/supermemory/types/related_memory.py create mode 100644 src/supermemory/types/related_memory_system.py create mode 100644 src/supermemory/types/s3config.py delete mode 100644 src/supermemory/types/search_documents_params.py delete mode 100644 src/supermemory/types/search_documents_response.py delete mode 100644 src/supermemory/types/search_execute_params.py delete mode 100644 src/supermemory/types/search_execute_response.py delete mode 100644 src/supermemory/types/search_memories_params.py delete mode 100644 src/supermemory/types/search_memories_response.py create mode 100644 src/supermemory/types/search_mode.py create mode 100644 src/supermemory/types/search_request_attach.py create mode 100644 src/supermemory/types/search_request_rerank.py create mode 100644 src/supermemory/types/search_response.py create mode 100644 src/supermemory/types/search_result.py create mode 100644 src/supermemory/types/search_result_included.py create mode 100644 src/supermemory/types/search_result_included_document.py create mode 100644 src/supermemory/types/search_result_system.py delete mode 100644 src/supermemory/types/setting_get_response.py delete mode 100644 src/supermemory/types/setting_update_params.py delete mode 100644 src/supermemory/types/setting_update_response.py create mode 100644 src/supermemory/types/sort_order.py create mode 100644 src/supermemory/types/task_type.py create mode 100644 src/supermemory/types/validation_error_response.py create mode 100644 src/supermemory/types/validation_error_response_error_item.py create mode 100644 src/supermemory/types/validation_error_response_error_item_path_item.py create mode 100644 src/supermemory/types/web_crawler_config.py delete mode 100644 src/supermemory_new/lib/.keep delete mode 100644 tests/__init__.py delete mode 100644 tests/api_resources/__init__.py delete mode 100644 tests/api_resources/test_client.py delete mode 100644 tests/api_resources/test_connections.py delete mode 100644 tests/api_resources/test_documents.py delete mode 100644 tests/api_resources/test_memories.py delete mode 100644 tests/api_resources/test_search.py delete mode 100644 tests/api_resources/test_settings.py delete mode 100644 tests/conftest.py create mode 100644 tests/live/test_live.py delete mode 100644 tests/sample_file.txt create mode 100644 tests/test_compat.py create mode 100644 tests/test_docs.py delete mode 100644 tests/test_extract_files.py delete mode 100644 tests/test_files.py delete mode 100644 tests/test_models.py delete mode 100644 tests/test_qs.py delete mode 100644 tests/test_required_args.py delete mode 100644 tests/test_response.py delete mode 100644 tests/test_streaming.py delete mode 100644 tests/test_transform.py delete mode 100644 tests/test_utils/test_datetime_parse.py delete mode 100644 tests/test_utils/test_json.py delete mode 100644 tests/test_utils/test_path.py delete mode 100644 tests/test_utils/test_proxy.py delete mode 100644 tests/test_utils/test_typing.py delete mode 100644 tests/utils.py create mode 100644 uv.lock diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index ff261bad..00000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -ARG VARIANT="3.9" -FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} - -USER vscode - -RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.44.0" RYE_INSTALL_OPTION="--yes" bash -ENV PATH=/home/vscode/.rye/shims:$PATH - -RUN echo "[[ -d .venv ]] && source .venv/bin/activate || export PATH=\$PATH" >> /home/vscode/.bashrc diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index c17fdc16..00000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,43 +0,0 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/debian -{ - "name": "Debian", - "build": { - "dockerfile": "Dockerfile", - "context": ".." - }, - - "postStartCommand": "rye sync --all-features", - - "customizations": { - "vscode": { - "extensions": [ - "ms-python.python" - ], - "settings": { - "terminal.integrated.shell.linux": "/bin/bash", - "python.pythonPath": ".venv/bin/python", - "python.defaultInterpreterPath": ".venv/bin/python", - "python.typeChecking": "basic", - "terminal.integrated.env.linux": { - "PATH": "/home/vscode/.rye/shims:${env:PATH}" - } - } - } - }, - "features": { - "ghcr.io/devcontainers/features/node:1": {} - } - - // Features to add to the dev container. More info: https://containers.dev/features. - // "features": {}, - - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - - // Configure tool-specific properties. - // "customizations": {}, - - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" -} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b39b80c..228b1af9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,104 +1,70 @@ name: CI on: push: - branches: - - '**' - - '!integrated/**' - - '!stl-preview-head/**' - - '!stl-preview-base/**' - - '!generated' - - '!codegen/**' - - 'codegen/stl/**' + branches: [main] pull_request: - branches-ignore: - - 'stl-preview-head/**' - - 'stl-preview-base/**' + workflow_dispatch: + +permissions: + contents: read jobs: - lint: - timeout-minutes: 10 - name: lint - runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} - if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') + test: + name: test (py${{ matrix.python }}${{ matrix.pydantic == 'v1' && ', pydantic v1' || '' }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python: ["3.10", "3.11", "3.12", "3.13", "3.14"] + pydantic: [v2] + include: + - python: "3.10" + pydantic: v1 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Install Rye - run: | - curl -sSf https://rye.astral.sh/get | bash - echo "$HOME/.rye/shims" >> $GITHUB_PATH - env: - RYE_VERSION: '0.44.0' - RYE_INSTALL_OPTION: '--yes' + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + - if: matrix.pydantic == 'v2' + run: uv run --python ${{ matrix.python }} --group dev pytest -q + - if: matrix.pydantic == 'v1' + run: uv run --python ${{ matrix.python }} --with 'pydantic<2' --group dev pytest -q - - name: Install dependencies - run: rye sync --all-features - - - name: Run lints - run: ./scripts/lint - - build: - if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') - timeout-minutes: 10 - name: build - permissions: - contents: read - id-token: write - runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + lint: + runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Install Rye - run: | - curl -sSf https://rye.astral.sh/get | bash - echo "$HOME/.rye/shims" >> $GITHUB_PATH - env: - RYE_VERSION: '0.44.0' - RYE_INSTALL_OPTION: '--yes' - - - name: Install dependencies - run: rye sync --all-features - - - name: Run build - run: rye build - - - name: Get GitHub OIDC Token - if: |- - github.repository == 'stainless-sdks/supermemory-new-python' && - !startsWith(github.ref, 'refs/heads/stl/') - id: github-oidc - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - script: core.setOutput('github_token', await core.getIDToken()); - - - name: Upload tarball - if: |- - github.repository == 'stainless-sdks/supermemory-new-python' && - !startsWith(github.ref, 'refs/heads/stl/') - env: - URL: https://pkg.stainless.com/s - AUTH: ${{ steps.github-oidc.outputs.github_token }} - SHA: ${{ github.sha }} - run: ./scripts/utils/upload-artifact.sh + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + - run: uv run --group dev ruff check . + - run: uv run --group dev ruff format --check custom tests scripts examples + - run: uv run --group dev mypy + - run: uv build - test: - timeout-minutes: 10 - name: test - runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + # src/supermemory must be exactly what scripts/generate produces from the + # committed spec + fern/ + custom/ — no hand edits. + generated: + runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Install Rye + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - uses: astral-sh/setup-uv@v6 + - run: SKIP_FETCH=1 scripts/generate + - name: Check for drift run: | - curl -sSf https://rye.astral.sh/get | bash - echo "$HOME/.rye/shims" >> $GITHUB_PATH - env: - RYE_VERSION: '0.44.0' - RYE_INSTALL_OPTION: '--yes' + if [ -n "$(git status --porcelain)" ]; then + git status --short + echo "::error::src/supermemory doesn't match scripts/generate. Edit fern/ or custom/ instead of src/, then run SKIP_FETCH=1 scripts/generate and commit." + exit 1 + fi - - name: Bootstrap - run: ./scripts/bootstrap - - - name: Run tests - run: ./scripts/test + # Against the real API; runs when the SUPERMEMORY_LIVE_API_KEY secret exists. + live: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + env: + SUPERMEMORY_LIVE_API_KEY: ${{ secrets.SUPERMEMORY_LIVE_API_KEY }} + steps: + - uses: actions/checkout@v4 + if: env.SUPERMEMORY_LIVE_API_KEY != '' + - uses: astral-sh/setup-uv@v6 + if: env.SUPERMEMORY_LIVE_API_KEY != '' + - if: env.SUPERMEMORY_LIVE_API_KEY != '' + run: uv run --group dev pytest tests/live -q diff --git a/.github/workflows/generate.yml b/.github/workflows/generate.yml new file mode 100644 index 00000000..59de4ded --- /dev/null +++ b/.github/workflows/generate.yml @@ -0,0 +1,75 @@ +name: Generate SDK +on: + # Sent by the API deploy when /v5/openapi changes: + # gh api repos/supermemoryai/python-sdk/dispatches -f event_type=openapi-updated + repository_dispatch: + types: [openapi-updated] + # Backstop in case a deploy didn't dispatch. + schedule: + - cron: "0 0 * * *" + workflow_dispatch: + inputs: + version: + description: Set the SDK version (PEP 440, e.g. 5.0.0rc2 or 5.0.0). Empty = bump automatically if the SDK changed. + type: string + required: false + +permissions: + contents: write + pull-requests: write + +concurrency: + group: generate-sdk + cancel-in-progress: true + +jobs: + generate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - uses: astral-sh/setup-uv@v6 + + - name: Generate from https://api.supermemory.ai/v5/openapi + run: scripts/generate + + - name: Version + id: meta + env: + VERSION: ${{ inputs.version }} + run: | + if [ -n "$VERSION" ]; then + sed -i "s/^version = \".*\"/version = \"$VERSION\"/" pyproject.toml + SKIP_FETCH=1 scripts/generate # re-stamp the User-Agent version + elif [ -n "$(git status --porcelain -- src)" ]; then + python3 scripts/bump_version.py + SKIP_FETCH=1 scripts/generate + fi + echo "version=$(grep -m1 '^version' pyproject.toml | cut -d'"' -f2)" >> "$GITHUB_OUTPUT" + { + echo 'stat<> "$GITHUB_OUTPUT" + + - name: Test + run: uv run --group dev pytest -q + + # Opens (or updates) one PR; no-op if nothing changed. A PR opened with the + # default GITHUB_TOKEN doesn't trigger CI, so set CODEGEN_PR_TOKEN (a + # fine-grained PAT or app token with contents + pull-requests write). + - uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.CODEGEN_PR_TOKEN || github.token }} + branch: codegen/regenerate + delete-branch: true + commit-message: "chore: regenerate SDK from v5 OpenAPI spec (${{ steps.meta.outputs.version }})" + title: "chore: regenerate SDK (${{ steps.meta.outputs.version }})" + body: | + Regenerated `src/supermemory` from `https://api.supermemory.ai/v5/openapi` (`scripts/generate`). + + Merging publishes **supermemory ${{ steps.meta.outputs.version }}** to PyPI. + + ``` + ${{ steps.meta.outputs.stat }} + ``` diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index f00e5c46..89d0119e 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -1,31 +1,50 @@ -# This workflow is triggered when a GitHub release is created. -# It can also be run manually to re-publish to PyPI in case it failed for some reason. -# You can run this workflow by navigating to https://www.github.com/supermemoryai/python-sdk/actions/workflows/publish-pypi.yml -name: Publish PyPI +name: Publish to PyPI on: + push: + branches: [main] workflow_dispatch: - release: - types: [published] +permissions: + contents: write # tag + GitHub release jobs: publish: - name: publish runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 - - name: Install Rye + # Merges that don't bump the version (docs, CI tweaks) are no-ops. + - name: Check version + id: meta run: | - curl -sSf https://rye.astral.sh/get | bash - echo "$HOME/.rye/shims" >> $GITHUB_PATH + VERSION=$(grep -m1 '^version' pyproject.toml | cut -d'"' -f2) + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + if curl -fsS "https://pypi.org/pypi/supermemory/$VERSION/json" >/dev/null 2>&1; then + echo "supermemory $VERSION is already on PyPI — skipping." + echo "publish=false" >> "$GITHUB_OUTPUT" + else + echo "publish=true" >> "$GITHUB_OUTPUT" + fi + + - if: steps.meta.outputs.publish == 'true' + run: uv run --group dev pytest -q + + - if: steps.meta.outputs.publish == 'true' + run: uv build + + - name: Publish ${{ steps.meta.outputs.version }} + if: steps.meta.outputs.publish == 'true' + run: uv publish env: - RYE_VERSION: '0.44.0' - RYE_INSTALL_OPTION: '--yes' + UV_PUBLISH_TOKEN: ${{ secrets.SUPERMEMORY_PYPI_TOKEN || secrets.PYPI_TOKEN }} - - name: Publish to PyPI - run: | - bash ./bin/publish-pypi + - name: Tag and release + if: steps.meta.outputs.publish == 'true' env: - PYPI_TOKEN: ${{ secrets.SUPERMEMORY_PYPI_TOKEN || secrets.PYPI_TOKEN }} + GH_TOKEN: ${{ github.token }} + VERSION: ${{ steps.meta.outputs.version }} + run: | + flags=(--target "$GITHUB_SHA" --generate-notes) + [[ "$VERSION" =~ (a|b|rc|dev)[0-9]+$ ]] && flags+=(--prerelease) + gh release create "v$VERSION" dist/* "${flags[@]}" diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml deleted file mode 100644 index 92b857d6..00000000 --- a/.github/workflows/release-doctor.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Release Doctor -on: - pull_request: - branches: - - main - workflow_dispatch: - -jobs: - release_doctor: - name: release doctor - runs-on: ubuntu-latest - if: github.repository == 'supermemoryai/python-sdk' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') - - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Check release environment - run: | - bash ./bin/check-release-environment - env: - PYPI_TOKEN: ${{ secrets.SUPERMEMORY_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.gitignore b/.gitignore index 3824f4c4..53cbba2d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,10 @@ -.prism.log -.stdy.log -_dev - __pycache__ .mypy_cache - -dist - +.pytest_cache +.ruff_cache .venv +dist .idea - .env .envrc -codegen.log -Brewfile.lock.json +fern/openapi.sdk.json diff --git a/.python-version b/.python-version index 43077b24..c8cfe395 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.9.18 +3.10 diff --git a/.release-please-manifest.json b/.release-please-manifest.json deleted file mode 100644 index 93001cb8..00000000 --- a/.release-please-manifest.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - ".": "3.62.0" -} \ No newline at end of file diff --git a/.stats.yml b/.stats.yml deleted file mode 100644 index ac25d3cd..00000000 --- a/.stats.yml +++ /dev/null @@ -1,4 +0,0 @@ -configured_endpoints: 26 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/supermemory--inc/supermemory-new-8407ccda1aefcb2261c5d8e40e75857f6c4bd6bed8b735cde8f1d0ce3e09f7b9.yml -openapi_spec_hash: 985e35ae4dd7938b867d09be70af171d -config_hash: cde97ef3188581c5f4924c633ec33ddb diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 5b010307..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "python.analysis.importFormat": "relative", -} diff --git a/Brewfile b/Brewfile deleted file mode 100644 index 492ca37b..00000000 --- a/Brewfile +++ /dev/null @@ -1,2 +0,0 @@ -brew "rye" - diff --git a/CHANGELOG.md b/CHANGELOG.md index 18940425..3ffdcbd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## 5.0.0rc1 (2026-10-04) + +Generated from the Supermemory **v5 API** (`/v5/openapi`) with Fern's open-source generator, replacing Stainless. See [MIGRATION.md](MIGRATION.md). + +### Breaking + +- Namespace-first methods matching the TypeScript SDK: `client.add(namespace, ...)`, `client.search(namespace, query=...)`, `client.profile`, `client.list`, `documents.*`, `memories.*`, `profiles.*`, `namespaces.*`, `organization.*`. +- Container tags are replaced by namespaces; `q` → `query`, `custom_id` → `id`, `filters` → `filter`, `include` → `attach`. +- Requires Python 3.10+. + +### Kept from 3.x + +- Client options, environment variables, per-call `extra_headers` / `extra_query` / `extra_body` / `timeout`, `with_options`, the exception hierarchy, `with_raw_response`, `to_dict()` / `to_json()`, and dict-typed request params. + +### New + +- `client.connectors.*` for Notion, Google Drive, OneDrive, Gmail, GitHub, S3, web crawler, and Granola. +- Named types (`Document`, `Memory`, `SearchResult`, `Connector`, …) and typed filter expressions. + ## 3.62.0 (2026-09-14) Full Changelog: [v3.61.0...v3.62.0](https://github.com/supermemoryai/python-sdk/compare/v3.61.0...v3.62.0) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 7002cbe0..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,121 +0,0 @@ -## Setting up the environment - -### With Rye - -We use [Rye](https://rye.astral.sh/) to manage dependencies because it will automatically provision a Python environment with the expected Python version. To set it up, run: - -```sh -$ ./scripts/bootstrap -``` - -Or [install Rye manually](https://rye.astral.sh/guide/installation/) and run: - -```sh -$ rye sync --all-features -``` - -You can then run scripts using `rye run python script.py` or by activating the virtual environment: - -```sh -# Activate the virtual environment - https://docs.python.org/3/library/venv.html#how-venvs-work -$ source .venv/bin/activate - -# now you can omit the `rye run` prefix -$ python script.py -``` - -### Without Rye - -Alternatively if you don't want to install `Rye`, you can stick with the standard `pip` setup by ensuring you have the Python version specified in `.python-version`, create a virtual environment however you desire and then install dependencies using this command: - -```sh -$ pip install -r requirements-dev.lock -``` - -## Modifying/Adding code - -Most of the SDK is generated code. Modifications to code will be persisted between generations, but may -result in merge conflicts between manual patches and changes from the generator. The generator will never -modify the contents of the `src/supermemory/lib/` and `examples/` directories. - -## Adding and running examples - -All files in the `examples/` directory are not modified by the generator and can be freely edited or added to. - -```py -# add an example to examples/.py - -#!/usr/bin/env -S rye run python -… -``` - -```sh -$ chmod +x examples/.py -# run the example against your api -$ ./examples/.py -``` - -## Using the repository from source - -If you’d like to use the repository from source, you can either install from git or link to a cloned repository: - -To install via git: - -```sh -$ pip install git+ssh://git@github.com/supermemoryai/python-sdk.git -``` - -Alternatively, you can build from source and install the wheel file: - -Building this package will create two files in the `dist/` directory, a `.tar.gz` containing the source files and a `.whl` that can be used to install the package efficiently. - -To create a distributable version of the library, all you have to do is run this command: - -```sh -$ rye build -# or -$ python -m build -``` - -Then to install: - -```sh -$ pip install ./path-to-wheel-file.whl -``` - -## Running tests - -```sh -$ ./scripts/test -``` - -## Linting and formatting - -This repository uses [ruff](https://github.com/astral-sh/ruff) and -[black](https://github.com/psf/black) to format the code in the repository. - -To lint: - -```sh -$ ./scripts/lint -``` - -To format and fix all ruff issues automatically: - -```sh -$ ./scripts/format -``` - -## Publishing and releases - -Changes made to this repository via the automated release PR pipeline should publish to PyPI automatically. If -the changes aren't made through the automated pipeline, you may want to make releases manually. - -### Publish with a GitHub workflow - -You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/supermemoryai/python-sdk/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. - -### Publish manually - -If you need to manually release a package, you can run the `bin/publish-pypi` script with a `PYPI_TOKEN` set on -the environment. diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 00000000..4043dd9f --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,99 @@ +# Migrating from `supermemory` 3.x to 5.x + +Starting with **5.0.0**, the Python SDK is generated from the Supermemory **v5 API** (`/v5/openapi`) using Fern's open-source generator instead of Stainless. The method names match the TypeScript SDK ([sdk-ts](https://github.com/supermemoryai/sdk-ts)) v5. + +v5 is namespace-first. Every content operation is scoped to a namespace, which replaces container tags, and the namespace is passed as the first argument. + +```sh +pip install --pre supermemory # while 5.0 is in release candidate +``` + +## TL;DR + +Only the API methods change. Everything around them works as before: + +| | 3.x | 5.x | +|---|---|---| +| Import / auth / env vars | `from supermemory import Supermemory`, `SUPERMEMORY_API_KEY`, `SUPERMEMORY_BASE_URL` | unchanged | +| Client options | `api_key`, `base_url`, `timeout`, `max_retries`, `default_headers`, `default_query`, `http_client` | unchanged | +| Per-call options | `extra_headers`, `extra_query`, `extra_body`, `timeout`; `client.with_options(...)` | unchanged | +| Retries / timeout | 2 retries, 60s | unchanged | +| Errors | `supermemory.APIStatusError`, `NotFoundError`, `RateLimitError`, `APIConnectionError`, … | unchanged | +| Raw responses | `client.with_raw_response.(...).parse()` | unchanged | +| Models | `.to_dict()`, `.to_json()` | unchanged | +| Scoping | `container_tag` / `container_tags` in the body | `namespace` positional argument (one per call) | +| Add | `client.add(content=..., container_tag="u")` | `client.add("u", content=...)` | +| Search | `client.search.memories(q=...)` | `client.search("u", query=...)` | +| Profile | profile + optional search (`q`) | profile only. Run `client.search` alongside it | +| Python | 3.9+ | 3.10+ | + +## Method map + +| 3.x call | 5.x call | +|---|---| +| `client.add(...)` / `client.documents.add(...)` | `client.add(namespace, content=...)` | +| `client.search.memories(...)` / `client.search.documents(...)` | `client.search(namespace, query=...)` | +| `client.profile(...)` | `client.profile(namespace)` | +| `client.documents.list(...)` / `client.memories.list(...)` | `client.list(namespace, "documents" \| "chunks" \| "memories")` | +| `client.documents.get(id)` | `client.documents.get(namespace, id)` | +| `client.documents.update(id, ...)` | `client.documents.update(namespace, id, ...)` | +| `client.documents.delete(...)` / `delete_bulk(...)` | `client.documents.delete(namespace, ids=[...])` | +| `client.documents.batch_add(...)` | `client.documents.batch_add(namespace, documents=[...])` | +| `client.documents.upload_file(...)` | `client.documents.upload_file(namespace, file=...)` | +| — | `client.documents.replace_with_file(...)`, `update_file(...)` | +| `client.memories.forget(...)` | `client.memories.forget(namespace, ids=[...])` | +| — | `client.memories.forget_matching(...)` | +| — | `client.profiles.{get_buckets, set_buckets, delete_buckets}` | +| `client.connections.*` | `client.connectors.{list_providers, list, create, get, update, delete, sync}` | +| container tag management | `client.namespaces.{list, get, update, delete}` | +| `client.settings.{get, update}` | `client.organization.{get, update}` | + +### Renamed request fields + +| 3.x | 5.x | +|---|---| +| `container_tag` / `container_tags` | `namespace` (first argument) | +| `custom_id` | `id` | +| `q` | `query` | +| `filters` | `filter` (typed filter expression) | +| `include` | `attach` | +| search `timing` / `total` | `search_time` | + +## Profile no longer searches + +```python +import asyncio +from supermemory import AsyncSupermemory + +client = AsyncSupermemory() + +profile, search = await asyncio.gather( + client.profile("user_alex"), + client.search("user_alex", query="upcoming meetings"), +) +``` + +## Several container tags + +v5 operations take exactly one namespace. To search several, fan out and merge: + +```python +responses = await asyncio.gather(*(client.search(ns, query="roadmap") for ns in ["user_alex", "team_design"])) +results = [r for resp in responses for r in resp.results] +``` + +## Smaller differences + +- Response types are generated from the v5 spec, so their names changed (e.g. `SearchMemoriesResponse` became `SearchResponse`). Import them from `supermemory.types`. +- `client.with_streaming_response` is gone. None of the v5 endpoints stream. +- `NOT_GIVEN` is gone. Leave out an argument (or pass `None`) to omit it. + +## Removed + +These 3.x methods have no v5 endpoint yet. Stay on `supermemory<4` if you depend on them: + +- `client.settings.{reset, suggest_buckets}` and chunking settings +- `client.conversations.add` +- `client.documents.{list_processing, chunks, file_url}` +- container tag merge +- `client.memories.{add, update_memory}` diff --git a/README.md b/README.md index 7db0bb68..1b677726 100644 --- a/README.md +++ b/README.md @@ -1,168 +1,110 @@ -# Supermemory Python API library +# Supermemory Python SDK - -[![PyPI version](https://img.shields.io/pypi/v/supermemory.svg?label=pypi%20(stable))](https://pypi.org/project/supermemory/) +[![PyPI version](https://img.shields.io/pypi/v/supermemory.svg)](https://pypi.org/project/supermemory/) -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). +The official Python library for the [Supermemory](https://supermemory.ai) v5 API, with sync and async clients built on [httpx](https://github.com/encode/httpx). -It is generated with [Stainless](https://www.stainless.com/). - -## MCP Server - -Use the Supermemory MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. - -[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=supermemory-mcp&config=eyJuYW1lIjoic3VwZXJtZW1vcnktbWNwIiwidHJhbnNwb3J0IjoiaHR0cCIsInVybCI6Imh0dHBzOi8vc3VwZXJtZW1vcnktbmV3LnN0bG1jcC5jb20iLCJoZWFkZXJzIjp7Ingtc3VwZXJtZW1vcnktYXBpLWtleSI6Ik15IEFQSSBLZXkifX0) -[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22supermemory-mcp%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fsupermemory-new.stlmcp.com%22%2C%22headers%22%3A%7B%22x-supermemory-api-key%22%3A%22My%20API%20Key%22%7D%7D) - -> Note: You may need to set environment variables in your MCP client. - -## Documentation - -The REST API documentation can be found on [docs.supermemory.ai](https://docs.supermemory.ai). The full API of this library can be found in [api.md](api.md). +> **Upgrading from 3.x?** v5 moves to the namespace-first v5 API. See [MIGRATION.md](MIGRATION.md). ## Installation ```sh -# install from PyPI -pip install supermemory +pip install --pre supermemory # 5.0 release candidates ``` ## Usage -The full API of this library can be found in [api.md](api.md). - ```python -import os from supermemory import Supermemory -client = Supermemory( - api_key=os.environ.get("SUPERMEMORY_API_KEY"), # This is the default and can be omitted -) +client = Supermemory() # reads SUPERMEMORY_API_KEY; or Supermemory(api_key="...") -response = client.search.documents( - q="documents related to python", -) -print(response.results) -``` +client.add("user_alex", content="Alex prefers morning meetings.", id="pref-1") -While you can provide an `api_key` keyword argument, -we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/) -to add `SUPERMEMORY_API_KEY="My API Key"` to your `.env` file -so that your API Key is not stored in source control. +response = client.search("user_alex", query="when does alex like to meet?", limit=5) +for result in response.results: + print(result) +``` -## Async usage +Every content operation is scoped to a **namespace** (a user ID, project ID, or anything you want to isolate memories by), passed as the first argument. -Simply import `AsyncSupermemory` instead of `Supermemory` and use `await` with each API call: +### Async ```python -import os import asyncio from supermemory import AsyncSupermemory -client = AsyncSupermemory( - api_key=os.environ.get("SUPERMEMORY_API_KEY"), # This is the default and can be omitted -) +async_client = AsyncSupermemory() async def main() -> None: - response = await client.search.documents( - q="documents related to python", + profile, results = await asyncio.gather( + async_client.profile("user_alex"), + async_client.search("user_alex", query="upcoming meetings"), ) - print(response.results) asyncio.run(main()) ``` -Functionality between the synchronous and asynchronous clients is otherwise identical. +Install `supermemory[aiohttp]` to use aiohttp as the async transport. -### With aiohttp +## API surface -By default, the async client uses `httpx` for HTTP requests. However, for improved concurrency performance you may also use `aiohttp` as the HTTP backend. +| Call | Endpoint | +|---|---| +| `client.add(namespace, content=...)` | `POST /ns/{namespace}/document` | +| `client.search(namespace, query=...)` | `POST /ns/{namespace}/search` | +| `client.profile(namespace)` | `POST /ns/{namespace}/profile` | +| `client.list(namespace, "documents" \| "chunks" \| "memories")` | `POST /ns/{namespace}/list/{type}` | +| `client.documents.{get, update, delete, batch_add, upload_file, replace_with_file, update_file}` | `/ns/{namespace}/document…` | +| `client.memories.{forget, forget_matching}` | `/ns/{namespace}/memories…` | +| `client.profiles.{get_buckets, set_buckets, delete_buckets}` | `/ns/{namespace}/profile/buckets` | +| `client.connectors.{list_providers, list, create, get, update, delete, sync}` | `/connectors`, `/ns/{namespace}/connectors…` | +| `client.namespaces.{list, get, update, delete}` | `/ns`, `/ns/{namespace}` | +| `client.organization.{get, update}` | `/organization` | -You can enable this by installing `aiohttp`: +Full parameter reference: [reference.md](reference.md). -```sh -# install from PyPI -pip install supermemory[aiohttp] -``` +## Filtering -Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: +Filters are plain dicts (typed as `FilterExpressionParams`), combinable with `and` / `or`: ```python -import os -import asyncio -from supermemory import DefaultAioHttpClient -from supermemory import AsyncSupermemory - - -async def main() -> None: - async with AsyncSupermemory( - api_key=os.environ.get("SUPERMEMORY_API_KEY"), # This is the default and can be omitted - http_client=DefaultAioHttpClient(), - ) as client: - response = await client.search.documents( - q="documents related to python", - ) - print(response.results) - - -asyncio.run(main()) +response = client.search( + "user_alex", + query="launch plans", + filter={ + "operator": "and", + "operands": [ + {"field": "source", "operator": "eq", "value": "slack"}, + {"field": "priority", "operator": "gte", "value": 3}, + ], + }, +) ``` -## Using types - -Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like: +The same filter shape works for `client.list(...)` and `client.profile(...)`. -- Serializing back into JSON, `model.to_json()` -- Converting to a dictionary, `model.to_dict()` - -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`. - -## Nested params - -Nested parameters are dictionaries, typed using `TypedDict`, for example: +## Listing ```python -from supermemory import Supermemory - -client = Supermemory() - -response = client.memories.update_memory( - container_tag="user_123", - new_content="John now prefers light mode", - temporal_context={}, -) -print(response.temporal_context) +page = client.list("user_alex", "documents", page=1, limit=50, sort="createdAt", order="desc") +for document in page.documents or []: + print(document.id, document.title) +print(page.pagination) ``` ## 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.documents.upload_file( - file=Path("/path/to/file"), -) +with open("notes.pdf", "rb") as f: + client.documents.upload_file("user_alex", file=f) ``` -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 +## 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. - -When the API returns a non-success status code (that is, 4xx or 5xx -response), a subclass of `supermemory.APIStatusError` is raised, containing `status_code` and `response` properties. - -All errors inherit from `supermemory.APIError`. +Same exception classes as 3.x. Non-2xx responses raise a subclass of `supermemory.APIStatusError` with `status_code`, `body`, `message`, `request`, and `response`. Network failures raise `APIConnectionError` (`APITimeoutError` for timeouts): ```python import supermemory @@ -171,253 +113,83 @@ from supermemory import Supermemory client = Supermemory() try: - client.add( - content="content", - ) -except supermemory.APIConnectionError as e: - print("The server could not be reached") - print(e.__cause__) # an underlying Exception, likely raised within httpx. + client.documents.get("user_alex", "missing-id") +except supermemory.NotFoundError: + ... except supermemory.RateLimitError as e: - print("A 429 status code was received; we should back off a bit.") + print(e.response.headers.get("retry-after")) except supermemory.APIStatusError as e: - print("Another non-200-range status code was received") - print(e.status_code) - print(e.response) + print(e.status_code, e.body) +except supermemory.APIConnectionError: + ... ``` -Error codes are as follows: - -| Status Code | Error Type | -| ----------- | -------------------------- | -| 400 | `BadRequestError` | -| 401 | `AuthenticationError` | -| 403 | `PermissionDeniedError` | -| 404 | `NotFoundError` | -| 422 | `UnprocessableEntityError` | -| 429 | `RateLimitError` | -| >=500 | `InternalServerError` | -| N/A | `APIConnectionError` | +| Status | Error | +|---|---| +| 400 | `BadRequestError` | +| 401 | `AuthenticationError` | +| 403 | `PermissionDeniedError` | +| 404 | `NotFoundError` | +| 409 | `ConflictError` | +| 422 | `UnprocessableEntityError` | +| 429 | `RateLimitError` | +| >=500 | `InternalServerError` | -### Retries +## Retries, timeouts, and per-request options -Certain errors are automatically retried 2 times by default, with a short exponential backoff. -Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, -429 Rate Limit, and >=500 Internal errors are all retried by default. - -You can use the `max_retries` option to configure or disable retry settings: +Connection errors, 408, 429, and 5xx responses are retried twice with exponential backoff, and requests time out after 60 seconds (5 seconds to connect). All of these can be set on the client, or for one request with `with_options`: ```python -from supermemory import Supermemory - -# Configure the default for all requests: -client = Supermemory( - # default is 2 - max_retries=0, -) - -# Or, configure per-request: -client.with_options(max_retries=5).add( - content="content", -) -``` - -### Timeouts - -By default requests time out after 1 minute. You can configure this with a `timeout` option, -which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object: - -```python -from supermemory import Supermemory - -# Configure the default for all requests: -client = Supermemory( - # 20 seconds (default is 1 minute) - timeout=20.0, -) - -# More granular control: -client = Supermemory( - timeout=httpx.Timeout(60.0, read=5.0, write=10.0, connect=2.0), -) - -# Override per-request: -client.with_options(timeout=5.0).add( - content="content", -) -``` - -On timeout, an `APITimeoutError` is thrown. - -Note that requests that time out are [retried twice by default](#retries). - -## Advanced - -### Logging - -We use the standard library [`logging`](https://docs.python.org/3/library/logging.html) module. - -You can enable logging by setting the environment variable `SUPERMEMORY_LOG` to `info`. - -```shell -$ export SUPERMEMORY_LOG=info -``` - -Or to `debug` for more verbose logging. - -### How to tell whether `None` means `null` or missing - -In an API response, a field may be explicitly `null`, or missing entirely; in either case, its value is `None` in this library. You can differentiate the two cases with `.model_fields_set`: - -```py -if response.my_field is None: - if 'my_field' not in response.model_fields_set: - print('Got json like {}, without a "my_field" key present at all.') - else: - print('Got json like {"my_field": null}.') -``` - -### Accessing raw response data (e.g. headers) - -The "raw" Response object can be accessed by prefixing `.with_raw_response.` to any HTTP method call, e.g., - -```py -from supermemory import Supermemory - -client = Supermemory() -response = client.with_raw_response.add( - content="content", -) -print(response.headers.get('X-My-Header')) - -client = response.parse() # get the object that `add()` would have returned -print(client.id) -``` - -These methods return an [`APIResponse`](https://github.com/supermemoryai/python-sdk/tree/main/src/supermemory/_response.py) object. - -The async client returns an [`AsyncAPIResponse`](https://github.com/supermemoryai/python-sdk/tree/main/src/supermemory/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. - -#### `.with_streaming_response` - -The above interface eagerly reads the full response body when you make the request, which may not always be what you want. - -To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. - -```python -with client.with_streaming_response.add( - content="content", -) as response: - print(response.headers.get("X-My-Header")) - - for line in response.iter_lines(): - print(line) -``` - -The context manager is required so that the response will reliably be closed. - -### Making custom/undocumented requests - -This library is typed for convenient access to the documented API. - -If you need to access undocumented endpoints, params, or response properties, the library can still be used. - -#### Undocumented endpoints - -To make requests to undocumented endpoints, you can make requests using `client.get`, `client.post`, and other -http verbs. Options on the client will be respected (such as retries) when making this request. - -```py import httpx -response = client.post( - "/foo", - cast_to=httpx.Response, - body={"my_param": True}, -) - -print(response.headers.get("x-foo")) -``` - -#### Undocumented request params - -If you want to explicitly send an extra param, you can do so with the `extra_query`, `extra_body`, and `extra_headers` request -options. - -#### Undocumented response properties - -To access undocumented response properties, you can access the extra fields like `response.unknown_prop`. You -can also get all the extra fields on the Pydantic model as a dict with -[`response.model_extra`](https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel.model_extra). - -### Configuring the HTTP client +client = Supermemory(max_retries=5, timeout=20.0, default_headers={"X-App": "my-app"}) -You can directly override the [httpx client](https://www.python-httpx.org/api/#client) to customize it for your use case, including: - -- Support for [proxies](https://www.python-httpx.org/advanced/proxies/) -- Custom [transports](https://www.python-httpx.org/advanced/transports/) -- Additional [advanced](https://www.python-httpx.org/advanced/clients/) functionality - -```python -import httpx -from supermemory import Supermemory, DefaultHttpxClient - -client = Supermemory( - # Or use the `SUPERMEMORY_BASE_URL` env var - base_url="http://my.test.server.example.com:8083", - http_client=DefaultHttpxClient( - proxy="http://my.test.proxy.example.com", - transport=httpx.HTTPTransport(local_address="0.0.0.0"), - ), -) +client.with_options(max_retries=0, timeout=httpx.Timeout(5.0)).search("user_alex", query="...") ``` -You can also customize the client on a per-request basis by using `with_options()`: +Every method also takes `extra_headers`, `extra_query`, `extra_body`, and `timeout`: ```python -client.with_options(http_client=DefaultHttpxClient(...)) +client.search("user_alex", query="...", extra_headers={"X-Trace": "1"}, timeout=5) ``` -### Managing HTTP resources - -By default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting. - -```py -from supermemory import Supermemory +The client reads `SUPERMEMORY_API_KEY`, `SUPERMEMORY_BASE_URL`, and `SUPERMEMORY_CUSTOM_HEADERS` from the environment. -with Supermemory() as client: - # make requests here - ... +## Raw responses -# HTTP client is now closed +```python +response = client.with_raw_response.search("user_alex", query="...") +print(response.headers.get("x-request-id")) +results = response.parse() # SearchResponse ``` -## Versioning - -This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions: - -1. Changes that only affect static types, without breaking runtime behavior. -2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_ -3. Changes that we do not expect to impact the vast majority of users in practice. - -We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. +Response models have `.to_dict()` and `.to_json()`. -We are keen for your feedback; please open an [issue](https://www.github.com/supermemoryai/python-sdk/issues) with questions, bugs, or suggestions. +## Development -### Determining the installed version +The SDK is generated from the live v5 OpenAPI spec with [Fern](https://buildwithfern.com)'s open-source Python generator, run locally in Docker. No hosted codegen service is involved. Don't edit `src/supermemory` by hand; CI fails if it doesn't match a fresh generation. -If you've upgraded to the latest version but aren't seeing any new features you were expecting then your python environment is likely still using an older version. +| To change | Edit | +|---|---| +| Method names, grouping, parameter names, hidden endpoints | `fern/overlay.yaml` (`x-fern-sdk-group-name`, `x-fern-sdk-method-name`, `x-fern-parameter-name`, `x-fern-ignore`) | +| Type names (`Document`, `Memory`, `Connector`, …) | `fern/schemas.yaml` | +| Generator options | `fern/generators.yml` | +| Client conventions (options, errors, per-call options) | `custom/supermemory/_compat.py` | +| Small fixes to Fern's generated core | `custom/patches/*.patch` | -You can determine the version that is being used at runtime with: +`scripts/generate` fetches the spec, names its schemas, runs Fern, then layers `custom/` on top. -```py -import supermemory -print(supermemory.__version__) +```sh +scripts/generate # fetch /v5/openapi and regenerate (needs Docker, bun, uv) +SKIP_FETCH=1 scripts/generate # regenerate from the committed fern/openapi.json +uv run --group dev pytest # offline tests, including every snippet in README.md and MIGRATION.md +SUPERMEMORY_LIVE_API_KEY=... uv run --group dev pytest tests/live # against the real API ``` -## Requirements - -Python 3.9 or higher. +Runnable examples are in [`examples/`](examples). -## Contributing +### Releases -See [the contributing documentation](./CONTRIBUTING.md). +- When the API deploy sends `openapi-updated` (or on the daily backstop run), **Generate SDK** regenerates. If the SDK changed, it bumps the version and opens a PR with the tests already run. +- Merging a PR that changes the version in `pyproject.toml` publishes it to PyPI and creates a GitHub release. +- To cut a specific version, run **Generate SDK** manually with `version` set. diff --git a/api.md b/api.md deleted file mode 100644 index db480fd5..00000000 --- a/api.md +++ /dev/null @@ -1,113 +0,0 @@ -# Supermemory - -Types: - -```python -from supermemory.types import AddResponse, ProfileResponse -``` - -Methods: - -- client.add(\*\*params) -> AddResponse -- client.profile(\*\*params) -> ProfileResponse - -# Memories - -Types: - -```python -from supermemory.types import MemoryForgetResponse, MemoryUpdateMemoryResponse -``` - -Methods: - -- client.memories.forget(\*\*params) -> MemoryForgetResponse -- client.memories.update_memory(\*\*params) -> MemoryUpdateMemoryResponse - -# Documents - -Types: - -```python -from supermemory.types import ( - DocumentUpdateResponse, - DocumentListResponse, - DocumentAddResponse, - DocumentBatchAddResponse, - DocumentDeleteBulkResponse, - DocumentGetResponse, - DocumentListProcessingResponse, - DocumentUploadFileResponse, -) -``` - -Methods: - -- client.documents.update(id, \*\*params) -> DocumentUpdateResponse -- client.documents.list(\*\*params) -> DocumentListResponse -- client.documents.delete(id) -> None -- client.documents.add(\*\*params) -> DocumentAddResponse -- client.documents.batch_add(\*\*params) -> DocumentBatchAddResponse -- client.documents.delete_bulk(\*\*params) -> DocumentDeleteBulkResponse -- client.documents.get(id) -> DocumentGetResponse -- client.documents.list_processing(\*\*params) -> DocumentListProcessingResponse -- client.documents.upload_file(\*\*params) -> DocumentUploadFileResponse - -# Search - -Types: - -```python -from supermemory.types import SearchDocumentsResponse, SearchExecuteResponse, SearchMemoriesResponse -``` - -Methods: - -- client.search.documents(\*\*params) -> SearchDocumentsResponse -- client.search.execute(\*\*params) -> SearchExecuteResponse -- client.search.memories(\*\*params) -> SearchMemoriesResponse - -# Settings - -Types: - -```python -from supermemory.types import SettingUpdateResponse, SettingGetResponse -``` - -Methods: - -- client.settings.update(\*\*params) -> SettingUpdateResponse -- client.settings.get() -> SettingGetResponse - -# Connections - -Types: - -```python -from supermemory.types import ( - ConnectionCreateResponse, - ConnectionListResponse, - ConnectionConfigureResponse, - ConnectionDeleteByIDResponse, - ConnectionDeleteByProviderResponse, - ConnectionGetByIDResponse, - ConnectionGetByTagResponse, - ConnectionImportResponse, - ConnectionListDocumentsResponse, - ConnectionResourcesResponse, -) -``` - -Methods: - -- client.connections.create(provider, \*\*params) -> ConnectionCreateResponse -- client.connections.list(\*\*params) -> ConnectionListResponse -- client.connections.configure(connection_id, \*\*params) -> ConnectionConfigureResponse -- client.connections.delete_by_id(connection_id, \*\*params) -> ConnectionDeleteByIDResponse -- client.connections.delete_by_provider(provider, \*\*params) -> ConnectionDeleteByProviderResponse -- client.connections.get_by_id(connection_id) -> ConnectionGetByIDResponse -- client.connections.get_by_tag(provider, \*\*params) -> ConnectionGetByTagResponse -- client.connections.import\_(provider, \*\*params) -> str -- client.connections.list_documents(provider, \*\*params) -> ConnectionListDocumentsResponse -- client.connections.resources(connection_id, \*\*params) -> ConnectionResourcesResponse diff --git a/bin/check-release-environment b/bin/check-release-environment deleted file mode 100644 index b845b0f4..00000000 --- a/bin/check-release-environment +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash - -errors=() - -if [ -z "${PYPI_TOKEN}" ]; then - errors+=("The PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") -fi - -lenErrors=${#errors[@]} - -if [[ lenErrors -gt 0 ]]; then - echo -e "Found the following errors in the release environment:\n" - - for error in "${errors[@]}"; do - echo -e "- $error\n" - done - - exit 1 -fi - -echo "The environment is ready to push releases!" diff --git a/bin/publish-pypi b/bin/publish-pypi deleted file mode 100644 index 826054e9..00000000 --- a/bin/publish-pypi +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -set -eux -mkdir -p dist -rye build --clean -rye publish --yes --token=$PYPI_TOKEN diff --git a/custom/__init__.tail.py b/custom/__init__.tail.py new file mode 100644 index 00000000..0b197a88 --- /dev/null +++ b/custom/__init__.tail.py @@ -0,0 +1,14 @@ +# --------------------------------------------------------------------------- +# Hand-written 3.x-compatible surface (custom/supermemory/_compat.py), appended +# by scripts/apply_custom.py, which also drops these names from the lazy-import +# tables above so `supermemory.Supermemory`, `supermemory.NotFoundError`, … are +# the compatible versions. +from importlib.metadata import version as _package_version + +from ._compat import * +from ._compat import __all__ as _compat_all + +__title__ = "supermemory" +__version__ = _package_version("supermemory") +__all__ += [name for name in _compat_all if name not in __all__] +__all__ += ["__title__", "__version__"] diff --git a/custom/patches/0001-compat-response-and-model-helpers.patch b/custom/patches/0001-compat-response-and-model-helpers.patch new file mode 100644 index 00000000..b2b33a1a --- /dev/null +++ b/custom/patches/0001-compat-response-and-model-helpers.patch @@ -0,0 +1,93 @@ +diff -ru a/src/supermemory/core/http_response.py b/src/supermemory/core/http_response.py +--- a/src/supermemory/core/http_response.py 2026-10-04 21:47:52 ++++ b/src/supermemory/core/http_response.py 2026-10-04 21:47:52 +@@ -24,7 +24,12 @@ + def status_code(self) -> int: + return self._response.status_code + ++ @property ++ def http_response(self) -> httpx.Response: ++ """The underlying httpx response (3.x-compatible).""" ++ return self._response + ++ + class HttpResponse(Generic[T], BaseHttpResponse): + """HTTP response wrapper that exposes response headers and data.""" + +@@ -38,6 +43,10 @@ + def data(self) -> T: + return self._data + ++ def parse(self) -> T: ++ """The parsed response body (3.x-compatible alias of `.data`).""" ++ return self._data ++ + def close(self) -> None: + self._response.close() + +@@ -53,6 +62,10 @@ + + @property + def data(self) -> T: ++ return self._data ++ ++ async def parse(self) -> T: ++ """The parsed response body (3.x-compatible alias of `.data`).""" + return self._data + + async def close(self) -> None: +diff -ru a/src/supermemory/core/unchecked_base_model.py b/src/supermemory/core/unchecked_base_model.py +--- a/src/supermemory/core/unchecked_base_model.py 2026-10-04 21:47:52 ++++ b/src/supermemory/core/unchecked_base_model.py 2026-10-04 21:47:52 +@@ -65,6 +65,51 @@ + class Config: + extra = pydantic.Extra.allow + ++ def to_dict( ++ self, ++ *, ++ mode: typing.Literal["json", "python"] = "python", ++ use_api_names: bool = True, ++ exclude_unset: bool = True, ++ exclude_defaults: bool = False, ++ exclude_none: bool = False, ++ ) -> typing.Dict[str, typing.Any]: ++ """Dictionary of the model, keyed by API field names unless `use_api_names=False` (3.x-compatible).""" ++ options: typing.Dict[str, typing.Any] = { ++ "by_alias": use_api_names, ++ "exclude_unset": exclude_unset, ++ "exclude_defaults": exclude_defaults, ++ "exclude_none": exclude_none, ++ } ++ if IS_PYDANTIC_V2: ++ return pydantic.BaseModel.model_dump(self, mode=mode, **options) # type: ignore[no-any-return] ++ if mode == "json": ++ import json ++ ++ return json.loads(pydantic.BaseModel.json(self, **options)) # type: ignore[no-any-return] ++ return pydantic.BaseModel.dict(self, **options) ++ ++ def to_json( ++ self, ++ *, ++ indent: typing.Optional[int] = 2, ++ use_api_names: bool = True, ++ exclude_unset: bool = True, ++ exclude_defaults: bool = False, ++ exclude_none: bool = False, ++ ) -> str: ++ """JSON string of the model, keyed by API field names unless `use_api_names=False` (3.x-compatible).""" ++ import json ++ ++ data = self.to_dict( ++ mode="json", ++ use_api_names=use_api_names, ++ exclude_unset=exclude_unset, ++ exclude_defaults=exclude_defaults, ++ exclude_none=exclude_none, ++ ) ++ return json.dumps(data, indent=indent) ++ + @classmethod + def model_construct( + cls: typing.Type["Model"], diff --git a/custom/patches/0002-resolve-recursive-forward-refs.patch b/custom/patches/0002-resolve-recursive-forward-refs.patch new file mode 100644 index 00000000..66d4eee0 --- /dev/null +++ b/custom/patches/0002-resolve-recursive-forward-refs.patch @@ -0,0 +1,62 @@ +diff -ru a/src/supermemory/core/serialization.py b/src/supermemory/core/serialization.py +--- a/src/supermemory/core/serialization.py 2026-10-04 21:59:07 ++++ b/src/supermemory/core/serialization.py 2026-10-04 21:59:07 +@@ -1,6 +1,7 @@ + # This file was auto-generated by Fern from our API Definition. + + import collections ++import importlib + import inspect + import typing + +@@ -45,10 +46,41 @@ + return cached + + ++class _SdkNamespace(dict): # type: ignore[type-arg] ++ """Resolves names against the SDK's top-level (lazily imported) exports.""" ++ ++ def __missing__(self, name: str) -> typing.Any: ++ root = importlib.import_module(__name__.split(".")[0]) ++ try: ++ return getattr(root, name) ++ except AttributeError: ++ raise KeyError(name) from None ++ ++ ++# Recursive types (e.g. a filter whose operands are filters) import each other only under ++# TYPE_CHECKING, so their forward references can't be resolved from their own module. ++_SDK_NAMESPACE = _SdkNamespace() ++_forward_ref_cache: typing.Dict[str, typing.Any] = {} ++ ++ ++def _resolve_forward_ref(ref: typing.ForwardRef) -> typing.Any: ++ name = ref.__forward_arg__ ++ if name not in _forward_ref_cache: ++ try: ++ _forward_ref_cache[name] = eval(name, {"typing": typing, "typing_extensions": typing_extensions}, _SDK_NAMESPACE) ++ except NameError: ++ return ref ++ return _forward_ref_cache[name] ++ ++ + def _resolve_type_hints(expected_type: typing.Any) -> typing.Dict[str, typing.Any]: + try: + return typing_extensions.get_type_hints(expected_type, include_extras=True) + except NameError: ++ pass ++ try: ++ return typing_extensions.get_type_hints(expected_type, localns=_SDK_NAMESPACE, include_extras=True) ++ except NameError: + # The type contains a circular reference, so we use the __annotations__ attribute directly. + return getattr(expected_type, "__annotations__", {}) + +@@ -278,6 +310,8 @@ + + + def _remove_annotations(type_: typing.Any) -> typing.Any: ++ if isinstance(type_, typing.ForwardRef): ++ type_ = _resolve_forward_ref(type_) + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return type_ diff --git a/custom/supermemory/_compat.py b/custom/supermemory/_compat.py new file mode 100644 index 00000000..a9921870 --- /dev/null +++ b/custom/supermemory/_compat.py @@ -0,0 +1,541 @@ +"""3.x client conventions on top of the Fern-generated v5 client. + +Hand-written — lives in custom/ and is copied into src/supermemory by +scripts/generate. It never names an endpoint: methods are wrapped generically, +so spec changes regenerate cleanly without touching this file. + +Kept from the 3.x (Stainless) SDK: +- constructor options: api_key, base_url, timeout, max_retries, default_headers, + default_query, http_client; SUPERMEMORY_API_KEY / SUPERMEMORY_BASE_URL / + SUPERMEMORY_CUSTOM_HEADERS +- per-call options: extra_headers, extra_query, extra_body, timeout +- client.with_options(...) / client.copy(...) +- the exception hierarchy (SupermemoryError → APIError → APIStatusError → + NotFoundError, RateLimitError, …; APIConnectionError, APITimeoutError) with + .status_code, .body, .message, .request, .response +- with_raw_response → .parse(), .headers, .status_code, .http_response, and + model.to_dict() / model.to_json() (added to generated core by custom/patches/) +""" + +from __future__ import annotations + +import contextvars +import functools +import inspect +import os +from collections.abc import Callable, Mapping +from typing import Any, Literal, cast + +import httpx +import pydantic +from typing_extensions import Self + +from .client import AsyncSupermemory as _GeneratedAsyncSupermemory +from .client import Supermemory as _GeneratedSupermemory +from .core.api_error import ApiError +from .core.pydantic_utilities import IS_PYDANTIC_V2 + +__all__ = [ + "DEFAULT_MAX_RETRIES", + "DEFAULT_TIMEOUT", + "APIConnectionError", + "APIError", + "APIResponseValidationError", + "APIStatusError", + "APITimeoutError", + "AsyncClient", + "AsyncSupermemory", + "AuthenticationError", + "BadRequestError", + "Client", + "ConflictError", + "InternalServerError", + "NotFoundError", + "PermissionDeniedError", + "RateLimitError", + "Supermemory", + "SupermemoryError", + "Timeout", + "UnprocessableEntityError", +] + +Timeout = httpx.Timeout +DEFAULT_TIMEOUT = httpx.Timeout(timeout=60, connect=5.0) +DEFAULT_MAX_RETRIES = 2 + +# ----------------------------------------------------------------- exceptions + + +class SupermemoryError(Exception): + pass + + +class APIError(SupermemoryError): + message: str + request: httpx.Request | None + body: object + + def __init__(self, message: str, request: httpx.Request | None, *, body: object) -> None: + # Not super(): raised errors also inherit the generated ApiError, whose + # keyword-only __init__ would be next in the MRO. + Exception.__init__(self, message) + self.request = request + self.message = message + self.body = body + + +class APIResponseValidationError(APIError): + response: httpx.Response + status_code: int + + def __init__(self, response: httpx.Response, body: object, *, message: str | None = None) -> None: + super().__init__(message or "Data returned by API invalid for expected schema.", response.request, body=body) + self.response = response + self.status_code = response.status_code + + +class APIStatusError(APIError): + """Raised when an API response has a status code of 4xx or 5xx.""" + + response: httpx.Response | None + status_code: int + headers: dict[str, str] | None + + def __init__(self, message: str, *, response: httpx.Response | None, body: object, status_code: int) -> None: + super().__init__(message, response.request if response is not None else None, body=body) + self.response = response + self.status_code = status_code + self.headers = dict(response.headers) if response is not None else None + + +class APIConnectionError(APIError): + def __init__(self, *, message: str = "Connection error.", request: httpx.Request | None) -> None: + super().__init__(message, request, body=None) + + +class APITimeoutError(APIConnectionError): + def __init__(self, request: httpx.Request | None) -> None: + super().__init__(message="Request timed out.", request=request) + + +class BadRequestError(APIStatusError): + status_code: Literal[400] = 400 # type: ignore[assignment] + + +class AuthenticationError(APIStatusError): + status_code: Literal[401] = 401 # type: ignore[assignment] + + +class PermissionDeniedError(APIStatusError): + status_code: Literal[403] = 403 # type: ignore[assignment] + + +class NotFoundError(APIStatusError): + status_code: Literal[404] = 404 # type: ignore[assignment] + + +class ConflictError(APIStatusError): + status_code: Literal[409] = 409 # type: ignore[assignment] + + +class UnprocessableEntityError(APIStatusError): + status_code: Literal[422] = 422 # type: ignore[assignment] + + +class RateLimitError(APIStatusError): + status_code: Literal[429] = 429 # type: ignore[assignment] + + +class InternalServerError(APIStatusError): + pass + + +_STATUS_ERRORS: dict[int, type[APIStatusError]] = { + 400: BadRequestError, + 401: AuthenticationError, + 403: PermissionDeniedError, + 404: NotFoundError, + 409: ConflictError, + 422: UnprocessableEntityError, + 429: RateLimitError, +} + +# The httpx.Response behind the most recent request in this thread / task, so +# status errors can carry .response and .request like 3.x did. +_last_response: contextvars.ContextVar[httpx.Response | None] = contextvars.ContextVar( + "supermemory_last_response", default=None +) + + +def _capture_response(response: httpx.Response) -> None: + _last_response.set(response) + + +async def _acapture_response(response: httpx.Response) -> None: + _last_response.set(response) + + +_combined_error_classes: dict[tuple[type[ApiError], type[APIStatusError]], type[APIStatusError]] = {} + + +def _combined_error_class(generated: type[ApiError], compat: type[APIStatusError]) -> type[APIStatusError]: + # Subclass of both, so `except supermemory.NotFoundError` (3.x) and + # `except supermemory.errors.NotFoundError` (generated) both catch it. + key = (generated, compat) + if key not in _combined_error_classes: + _combined_error_classes[key] = cast( + type[APIStatusError], + type(compat.__name__, (compat, generated), {"__module__": "supermemory", "__str__": Exception.__str__}), + ) + return _combined_error_classes[key] + + +def _status_error(err: ApiError) -> APIStatusError: + status = err.status_code or 0 + compat = _STATUS_ERRORS.get(status) or (InternalServerError if status >= 500 else APIStatusError) + response = _last_response.get() + if response is not None and response.status_code != status: + response = None + body = err.body + if response is not None: + # The decoded JSON (or text) the API sent, not a re-serialized model. + try: + body = response.json() + except ValueError: + body = response.text + elif isinstance(body, pydantic.BaseModel): + body = ( + body.model_dump(by_alias=True, exclude_unset=True) + if IS_PYDANTIC_V2 + else body.dict(by_alias=True, exclude_unset=True) + ) + cls = _combined_error_class(type(err), compat) + new = cls.__new__(cls) + APIStatusError.__init__(new, f"Error code: {status} - {body}", response=response, body=body, status_code=status) + if response is None: + new.headers = err.headers + return new + + +def _request_of(err: httpx.HTTPError) -> httpx.Request | None: + try: + return err.request + except RuntimeError: + return None + + +# ---------------------------------------------------------- per-call options + + +class _Defaults: + def __init__(self, default_query: Mapping[str, object] | None) -> None: + self.default_query = dict(default_query or {}) + + +def _apply_per_call_options(kwargs: dict[str, Any], defaults: _Defaults) -> None: + # scripts/apply_custom.py adds these parameters to every generated method + # signature (for type checkers); here they become Fern request_options. + extra_headers = kwargs.pop("extra_headers", None) + extra_query = kwargs.pop("extra_query", None) + extra_body = kwargs.pop("extra_body", None) + timeout = kwargs.pop("timeout", None) + query = {**defaults.default_query, **(extra_query or {})} + if not (extra_headers or query or extra_body or timeout is not None): + return + request_options: dict[str, Any] = dict(kwargs.get("request_options") or {}) + if extra_headers: + request_options["additional_headers"] = {**extra_headers, **request_options.get("additional_headers", {})} + if query: + request_options["additional_query_parameters"] = { + **query, + **request_options.get("additional_query_parameters", {}), + } + if extra_body: + request_options["additional_body_parameters"] = { + **extra_body, + **request_options.get("additional_body_parameters", {}), + } + if timeout is not None: + request_options["timeout"] = timeout.read if isinstance(timeout, httpx.Timeout) else timeout + kwargs["request_options"] = request_options + + +def _wrap_method(fn: Callable[..., Any], defaults: _Defaults) -> Callable[..., Any]: + if inspect.iscoroutinefunction(fn): + + @functools.wraps(fn) + async def async_wrapper(*args: Any, **kwargs: Any) -> Any: + _apply_per_call_options(kwargs, defaults) + try: + return await fn(*args, **kwargs) + except ApiError as err: + raise _status_error(err).with_traceback(err.__traceback__) from None + except httpx.TimeoutException as err: + raise APITimeoutError(request=_request_of(err)) from err + except httpx.TransportError as err: + raise APIConnectionError(request=_request_of(err)) from err + + return async_wrapper + + @functools.wraps(fn) + def wrapper(*args: Any, **kwargs: Any) -> Any: + _apply_per_call_options(kwargs, defaults) + try: + return fn(*args, **kwargs) + except ApiError as err: + raise _status_error(err).with_traceback(err.__traceback__) from None + except httpx.TimeoutException as err: + raise APITimeoutError(request=_request_of(err)) from err + except httpx.TransportError as err: + raise APIConnectionError(request=_request_of(err)) from err + + return wrapper + + +def _is_generated_client(value: object) -> bool: + cls = type(value) + return cls.__module__.startswith(f"{__package__}.") and cls.__name__.endswith(("Client", "Supermemory")) + + +def _wrap(name: str, value: Any, defaults: _Defaults) -> Any: + if name.startswith("_"): + return value + if inspect.ismethod(value) and "request_options" in inspect.signature(value).parameters: + return _wrap_method(value, defaults) + if _is_generated_client(value): + return _Resource(value, defaults) + return value + + +class _Resource: + """Wraps a generated sub-client (client.documents, client.with_raw_response, …).""" + + def __init__(self, target: Any, defaults: _Defaults) -> None: + self._target = target + self._defaults = defaults + + def __getattr__(self, name: str) -> Any: + return _wrap(name, getattr(self._target, name), self._defaults) + + def __dir__(self) -> Any: + return dir(self._target) + + def __repr__(self) -> str: + return repr(self._target) + + +# -------------------------------------------------------------------- clients + + +def _env_headers(default_headers: Mapping[str, str] | None) -> dict[str, str]: + headers: dict[str, str] = {} + for line in os.environ.get("SUPERMEMORY_CUSTOM_HEADERS", "").split("\n"): + colon = line.find(":") + if colon >= 0: + headers[line[:colon].strip()] = line[colon + 1 :].strip() + return {**headers, **(default_headers or {})} + + +def _resolve(api_key: str | None, base_url: str | httpx.URL | None) -> tuple[str, str]: + api_key = api_key if api_key is not None else os.environ.get("SUPERMEMORY_API_KEY") + if api_key is None: + raise SupermemoryError( + "The api_key client option must be set either by passing api_key to the client " + "or by setting the SUPERMEMORY_API_KEY environment variable" + ) + base_url = base_url if base_url is not None else os.environ.get("SUPERMEMORY_BASE_URL") + return api_key, str(base_url or "https://api.supermemory.ai").rstrip("/") + + +def _resolve_timeout(timeout: float | httpx.Timeout | None) -> httpx.Timeout: + return ( + timeout + if isinstance(timeout, httpx.Timeout) + else httpx.Timeout(timeout) + if timeout is not None + else DEFAULT_TIMEOUT + ) + + +def _add_hook(client: httpx.Client | httpx.AsyncClient, hook: Callable[..., Any]) -> None: + hooks = client.event_hooks + if hook not in hooks["response"]: + hooks["response"] = [*hooks["response"], hook] + client.event_hooks = hooks + + +_OWN_ATTRIBUTES = frozenset({"with_options", "copy", "api_key", "base_url", "timeout", "max_retries", "close"}) + + +class Supermemory(_GeneratedSupermemory): + """Synchronous Supermemory client (3.x-compatible options; v5 API methods).""" + + def __init__( + self, + *, + api_key: str | None = None, + base_url: str | httpx.URL | None = None, + timeout: float | httpx.Timeout | None = None, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + http_client: httpx.Client | None = None, + **kwargs: Any, + ) -> None: + self.api_key, self.base_url = _resolve(api_key, base_url) + self.timeout = _resolve_timeout(timeout) + self.max_retries = max_retries + headers = _env_headers({**(default_headers or {}), **kwargs.pop("headers", {})}) + http_client = http_client or kwargs.pop("httpx_client", None) + self._options: dict[str, Any] = dict( + api_key=self.api_key, + base_url=self.base_url, + timeout=self.timeout, + max_retries=max_retries, + default_headers=headers, + default_query=dict(default_query or {}), + http_client=http_client, + **kwargs, + ) + self._owned_http_client = http_client is None + self._http_client = http_client or httpx.Client(timeout=self.timeout, follow_redirects=True) + _add_hook(self._http_client, _capture_response) + self._defaults = _Defaults(default_query) + super().__init__( + api_key=self.api_key, + base_url=self.base_url, + timeout=self.timeout.read, + max_retries=max_retries, + headers=headers, + httpx_client=self._http_client, + **kwargs, + ) + + def __getattribute__(self, name: str) -> Any: + value = super().__getattribute__(name) + if name.startswith("_") or name in _OWN_ATTRIBUTES: + return value + return _wrap(name, value, super().__getattribute__("_defaults")) + + def copy( + self, + *, + set_default_headers: Mapping[str, str] | None = None, + set_default_query: Mapping[str, object] | None = None, + **overrides: Any, + ) -> Supermemory: + """A new client with some options changed; the rest, including the connection pool, are kept.""" + options = dict(self._options) + if self._owned_http_client: + options["http_client"] = self._http_client + options["default_headers"] = set_default_headers or { + **options["default_headers"], + **(overrides.pop("default_headers", None) or {}), + } + options["default_query"] = set_default_query or { + **options["default_query"], + **(overrides.pop("default_query", None) or {}), + } + options.update(overrides) + return type(self)(**options) + + with_options = copy + + def close(self) -> None: + if self._owned_http_client: + self._http_client.close() + + def __enter__(self) -> Self: + return self + + def __exit__(self, *_: object) -> None: + self.close() + + +class AsyncSupermemory(_GeneratedAsyncSupermemory): + """Asynchronous Supermemory client (3.x-compatible options; v5 API methods).""" + + def __init__( + self, + *, + api_key: str | None = None, + base_url: str | httpx.URL | None = None, + timeout: float | httpx.Timeout | None = None, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + http_client: httpx.AsyncClient | None = None, + **kwargs: Any, + ) -> None: + self.api_key, self.base_url = _resolve(api_key, base_url) + self.timeout = _resolve_timeout(timeout) + self.max_retries = max_retries + headers = _env_headers({**(default_headers or {}), **kwargs.pop("headers", {})}) + http_client = http_client or kwargs.pop("httpx_client", None) + self._options: dict[str, Any] = dict( + api_key=self.api_key, + base_url=self.base_url, + timeout=self.timeout, + max_retries=max_retries, + default_headers=headers, + default_query=dict(default_query or {}), + http_client=http_client, + **kwargs, + ) + self._owned_http_client = http_client is None + self._http_client = http_client or httpx.AsyncClient(timeout=self.timeout, follow_redirects=True) + _add_hook(self._http_client, _acapture_response) + self._defaults = _Defaults(default_query) + super().__init__( + api_key=self.api_key, + base_url=self.base_url, + timeout=self.timeout.read, + max_retries=max_retries, + headers=headers, + httpx_client=self._http_client, + **kwargs, + ) + + def __getattribute__(self, name: str) -> Any: + value = super().__getattribute__(name) + if name.startswith("_") or name in _OWN_ATTRIBUTES: + return value + return _wrap(name, value, super().__getattribute__("_defaults")) + + def copy( + self, + *, + set_default_headers: Mapping[str, str] | None = None, + set_default_query: Mapping[str, object] | None = None, + **overrides: Any, + ) -> AsyncSupermemory: + """A new client with some options changed; the rest, including the connection pool, are kept.""" + options = dict(self._options) + if self._owned_http_client: + options["http_client"] = self._http_client + options["default_headers"] = set_default_headers or { + **options["default_headers"], + **(overrides.pop("default_headers", None) or {}), + } + options["default_query"] = set_default_query or { + **options["default_query"], + **(overrides.pop("default_query", None) or {}), + } + options.update(overrides) + return type(self)(**options) + + with_options = copy + + async def close(self) -> None: + if self._owned_http_client: + await self._http_client.aclose() + + async def __aenter__(self) -> Self: + return self + + async def __aexit__(self, *_: object) -> None: + await self.close() + + +Client = Supermemory +AsyncClient = AsyncSupermemory diff --git a/examples/.keep b/examples/.keep deleted file mode 100644 index d8c73e93..00000000 --- a/examples/.keep +++ /dev/null @@ -1,4 +0,0 @@ -File generated from our OpenAPI spec by Stainless. - -This directory can be used to store example files demonstrating usage of this SDK. -It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/examples/chat_with_memory.py b/examples/chat_with_memory.py new file mode 100644 index 00000000..ce198856 --- /dev/null +++ b/examples/chat_with_memory.py @@ -0,0 +1,32 @@ +"""Give an assistant long-term memory: recall before answering, remember after. + +SUPERMEMORY_API_KEY=... uv run examples/chat_with_memory.py +""" + +import asyncio + +from supermemory import AsyncSupermemory + +client = AsyncSupermemory() + + +async def context_for(user_id: str, message: str) -> str: + # Profile and search are independent, so fetch them in parallel. + profile, recall = await asyncio.gather( + client.profile(user_id), + client.search(user_id, query=message, limit=5), + ) + facts = [fact.memory for fact in profile.profile.static or []] + memories = [result.memory or result.chunk or "" for result in recall.results] + return "\n".join(["Known facts:", *facts, "", "Relevant memories:", *memories]) + + +async def main() -> None: + user_id = "example_user" + message = "Can you schedule my 1:1 with Sam?" + print(await context_for(user_id, message)) + # ...call your model with that context, then store the exchange: + await client.add(user_id, content=f"User asked: {message}") + + +asyncio.run(main()) diff --git a/examples/errors_and_retries.py b/examples/errors_and_retries.py new file mode 100644 index 00000000..0473ac55 --- /dev/null +++ b/examples/errors_and_retries.py @@ -0,0 +1,22 @@ +"""Error handling, retries, and per-request options. + +SUPERMEMORY_API_KEY=... uv run examples/errors_and_retries.py +""" + +import supermemory +from supermemory import Supermemory + +client = Supermemory(max_retries=4, timeout=20.0) + +try: + client.with_options(max_retries=0).documents.get("example_user", "does-not-exist") +except supermemory.NotFoundError as e: + print("not found:", e.body) +except supermemory.RateLimitError as e: + print("rate limited; retry after", e.response.headers.get("retry-after") if e.response else "?") +except supermemory.APIConnectionError: + print("network problem") + +raw = client.with_raw_response.search("example_user", query="anything", extra_headers={"X-Trace": "demo"}) +print("request id:", raw.headers.get("x-request-id")) +print("results:", len(raw.parse().results)) diff --git a/examples/quickstart.py b/examples/quickstart.py new file mode 100644 index 00000000..cf89277f --- /dev/null +++ b/examples/quickstart.py @@ -0,0 +1,21 @@ +"""Add a memory, then recall it. + +SUPERMEMORY_API_KEY=... uv run examples/quickstart.py +""" + +import time + +from supermemory import Supermemory + +client = Supermemory() +namespace = "example_user" + +added = client.add(namespace, content="Alex prefers morning meetings and hates Mondays.", id="alex-prefs") +print(f"added {added.id} ({added.status})") + +# Ingestion is asynchronous; give it a moment before recalling. +time.sleep(5) + +response = client.search(namespace, query="When does Alex like to meet?", limit=3) +for result in response.results: + print(f"{result.similarity:.2f} {result.memory or result.chunk}") diff --git a/fern/fern.config.json b/fern/fern.config.json new file mode 100644 index 00000000..c9cc41a9 --- /dev/null +++ b/fern/fern.config.json @@ -0,0 +1 @@ +{ "organization": "supermemory", "version": "5.144.3" } diff --git a/fern/generators.yml b/fern/generators.yml new file mode 100644 index 00000000..1044cd4a --- /dev/null +++ b/fern/generators.yml @@ -0,0 +1,38 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +# +# Generates src/supermemory from the Supermemory v5 API (openapi.json, refreshed +# from https://api.supermemory.ai/v5/openapi by scripts/generate). Fern runs the +# open-source generator image locally in Docker — no hosted Fern account needed. +# +# overlay.yaml maps operations onto the SDK surface (client.add, documents.*, …) +# and keeps method names in line with the TypeScript SDK (supermemoryai/sdk-ts). +api: + specs: + # openapi.json with the type names from schemas.yaml (scripts/name_schemas.py) + - openapi: ./openapi.sdk.json + overlays: ./overlay.yaml + settings: + respect-nullable-schemas: true + coerce-enums-to-literals: true + path-parameter-order: url-order +default-group: python-sdk +groups: + python-sdk: + generators: + - name: fernapi/fern-python-sdk + version: 5.18.1 + output: + location: local-file-system + path: ../src/supermemory + config: + package_name: supermemory + client_class_name: Supermemory + # Body fields become keyword arguments: client.add("ns", content="…") + inline_request_params: true + # Nested request objects (filters, connector setup, batch documents) + # accept plain dicts, typed as TypedDicts — as in the 3.x SDK. + use_typeddict_requests: true + pydantic_config: + # Build response models without validation, so new API fields or + # enum values never break existing clients. + skip_validation: true diff --git a/fern/openapi.json b/fern/openapi.json new file mode 100644 index 00000000..cca2b6bf --- /dev/null +++ b/fern/openapi.json @@ -0,0 +1,8200 @@ +{ + "openapi": "3.1.0", + "components": { + "securitySchemes": { + "bearerAuth": { + "scheme": "bearer", + "type": "http" + } + }, + "schemas": { + "ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message", + "example": "Invalid request parameters" + }, + "details": { + "type": "string", + "description": "Additional error details", + "example": "Query must be at least 1 character long" + } + }, + "required": [ + "error" + ] + }, + "FilterPredicate": { + "anyOf": [ + { + "type": "object", + "properties": { + "field": { + "type": "string", + "minLength": 1, + "pattern": "^[a-zA-Z0-9_.-]+$", + "description": "Metadata key to evaluate, matched literally. A period is part of the key name, not a path into nested metadata." + }, + "operator": { + "type": "string", + "enum": [ + "eq", + "neq" + ], + "description": "Compare the field for equality or inequality" + }, + "value": { + "type": "string", + "description": "String value to compare against" + }, + "caseSensitive": { + "default": true, + "description": "Whether string comparison preserves letter case", + "type": "boolean" + } + }, + "required": [ + "field", + "operator", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "minLength": 1, + "pattern": "^[a-zA-Z0-9_.-]+$", + "description": "Metadata key to evaluate, matched literally. A period is part of the key name, not a path into nested metadata." + }, + "operator": { + "type": "string", + "enum": [ + "eq", + "neq" + ], + "description": "Compare the field for equality or inequality" + }, + "value": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "boolean" + } + ], + "description": "Numeric or boolean value to compare against" + } + }, + "required": [ + "field", + "operator", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "minLength": 1, + "pattern": "^[a-zA-Z0-9_.-]+$", + "description": "Metadata key to evaluate, matched literally. A period is part of the key name, not a path into nested metadata." + }, + "operator": { + "type": "string", + "enum": [ + "gt", + "gte", + "lt", + "lte" + ], + "description": "Numeric comparison to apply" + }, + "value": { + "type": "number", + "description": "Numeric value to compare against" + } + }, + "required": [ + "field", + "operator", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "minLength": 1, + "pattern": "^[a-zA-Z0-9_.-]+$", + "description": "Metadata key to evaluate, matched literally. A period is part of the key name, not a path into nested metadata." + }, + "operator": { + "type": "string", + "enum": [ + "contains", + "notContains" + ], + "description": "Require or exclude a substring match" + }, + "value": { + "type": "string", + "description": "Substring to look for" + }, + "caseSensitive": { + "default": true, + "description": "Whether substring matching preserves letter case", + "type": "boolean" + } + }, + "required": [ + "field", + "operator", + "value" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "minLength": 1, + "pattern": "^[a-zA-Z0-9_.-]+$", + "description": "Metadata key to evaluate, matched literally. A period is part of the key name, not a path into nested metadata." + }, + "operator": { + "type": "string", + "enum": [ + "arrayContains", + "arrayNotContains" + ], + "description": "Require or exclude an exact array member" + }, + "value": { + "type": "string", + "description": "Array member to look for" + } + }, + "required": [ + "field", + "operator", + "value" + ], + "additionalProperties": false + } + ] + }, + "FilterExpression": { + "description": "A type-safe metadata filter predicate or nested and/or expression. The API validates up to 5 levels of nesting.", + "anyOf": [ + { + "$ref": "#/components/schemas/FilterPredicate" + }, + { + "type": "object", + "properties": { + "operator": { + "type": "string", + "const": "and", + "description": "Require every nested filter expression to match" + }, + "operands": { + "type": "array", + "minItems": 1, + "maxItems": 200, + "items": { + "$ref": "#/components/schemas/FilterExpression" + }, + "description": "Filter expressions combined with logical AND" + } + }, + "required": [ + "operator", + "operands" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "operator": { + "type": "string", + "const": "or", + "description": "Require at least one nested filter expression to match" + }, + "operands": { + "type": "array", + "minItems": 1, + "maxItems": 200, + "items": { + "$ref": "#/components/schemas/FilterExpression" + }, + "description": "Filter expressions combined with logical OR" + } + }, + "required": [ + "operator", + "operands" + ], + "additionalProperties": false + } + ] + }, + "ValidationErrorResponse": { + "type": "object", + "description": "Body returned when request input fails schema validation", + "properties": { + "success": { + "type": "boolean", + "enum": [ + false + ] + }, + "error": { + "type": "array", + "description": "Validation issues", + "items": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "path": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + } + } + }, + "required": [ + "message" + ] + } + }, + "data": { + "description": "The input that failed validation" + } + }, + "required": [ + "success", + "error" + ] + } + } + }, + "info": { + "title": "supermemory API", + "description": "The Memory API for the AI era. OpenAPI operations include x-codeSamples for the official TypeScript and Python SDKs (Mintlify-compatible).", + "version": "5.0.0" + }, + "security": [ + { + "bearerAuth": [] + } + ], + "servers": [ + { + "description": "Production Server", + "url": "https://api.supermemory.ai" + } + ], + "tags": [ + { + "name": "Ingest", + "description": "Ingest documents, files, URLs, conversations, and other content" + }, + { + "name": "Recall (Search)", + "description": "Semantic recall across your content \u2014 supports memories, hybrid, and documents modes" + }, + { + "name": "Profiles", + "description": "Maintained static, dynamic, and custom-bucket profiles scoped by namespace" + }, + { + "name": "Content Management", + "description": "List, get, update, and delete content and memories" + }, + { + "name": "Namespaces", + "description": "List and manage isolated namespaces and their settings" + }, + { + "name": "Connectors", + "description": "Sync external sources such as Google Drive, Notion and GitHub into a namespace" + }, + { + "name": "Organization", + "description": "Read and update organization-wide settings" + } + ], + "paths": { + "/ns/{namespace}/document": { + "post": { + "operationId": "postNsByNamespaceDocument", + "description": "Turn text or a supported URL into searchable, evolving memory. Supply a new ID to create a document, or reuse an existing ID to append new information while preserving its history.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier of the document" + }, + "status": { + "type": "string", + "description": "Status of the document" + } + }, + "required": [ + "id", + "status" + ], + "additionalProperties": false + } + } + }, + "description": "Document accepted for processing" + }, + "400": { + "description": "Invalid request parameters", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "402": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Insufficient processing credits" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Namespace write access denied" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Namespace merge or filepath conflict" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server error" + } + }, + "summary": "Add document", + "tags": [ + "Ingest" + ], + "parameters": [ + { + "in": "path", + "name": "namespace", + "schema": { + "type": "string", + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_:-]+$", + "example": "user_alex" + }, + "required": true, + "description": "The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents." + }, + { + "in": "query", + "name": "taskType", + "schema": { + "type": "string", + "enum": [ + "memory", + "superrag" + ] + }, + "description": "Processing pipeline. \"memory\" builds durable learned context; \"superrag\" optimizes the document for retrieval without generating memories." + }, + { + "in": "query", + "name": "dreaming", + "schema": { + "type": "string", + "enum": [ + "dynamic", + "instant" + ] + }, + "description": "Processing mode. \"dynamic\" (default) groups related documents so memories form from coherent context. \"instant\" processes each document independently right away and bills one extra operation per document." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "The content to process. This may be plaintext or a URL to supported rich content.", + "example": "Supermemory turns unstructured content into evolving memory." + }, + "id": { + "description": "An optional caller-defined document ID", + "type": "string", + "minLength": 1, + "maxLength": 255, + "example": "my-doc-123" + }, + "supportingContext": { + "description": "Context used to guide memory extraction within this namespace. Max 1500 characters.", + "example": "Focus on product decisions, dates, and owners.", + "type": "string", + "maxLength": 1500 + }, + "metadata": { + "description": "Arbitrary metadata attached to the document and derived data", + "example": { + "source": "api-docs" + }, + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + "group": { + "description": "Metadata values that isolate related context within the namespace", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + "date": { + "description": "When the source content is from, in ISO 8601 format", + "example": "2026-01-15", + "type": "string" + } + }, + "required": [ + "content" + ], + "additionalProperties": false + } + } + } + }, + "x-codeSamples": [ + { + "lang": "bash", + "label": "cURL", + "source": "curl -X POST \"https://api.supermemory.ai/ns/user_123/document?dreaming=instant\" \\\n -H \"Authorization: Bearer $SUPERMEMORY_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"content\": \"Sarah is being promoted to VP of Product.\",\n \"id\": \"chat_session_1\",\n \"metadata\": { \"type\": \"conversation\" }\n }'" + } + ] + }, + "delete": { + "operationId": "deleteNsByNamespaceDocument", + "description": "Permanently remove documents and their derived knowledge by document ID or caller-defined ID. Each requested ID is handled independently so successful deletions are preserved when another ID fails.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "deletedCount": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991, + "description": "Number of documents successfully deleted" + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "example": "my-doc-123", + "description": "The requested document identifier" + }, + "error": { + "type": "string", + "minLength": 1, + "description": "Why this document could not be deleted" + } + }, + "required": [ + "id", + "error" + ], + "additionalProperties": false + }, + "description": "Per-document failures; successful deletions are not rolled back" + } + }, + "required": [ + "deletedCount", + "errors" + ], + "additionalProperties": false + } + } + }, + "description": "Document deletion completed" + }, + "400": { + "description": "Invalid delete request", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Write access required for this namespace" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal server error" + } + }, + "summary": "Delete documents", + "tags": [ + "Content Management" + ], + "parameters": [ + { + "in": "path", + "name": "namespace", + "schema": { + "type": "string", + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_:-]+$", + "example": "user_alex" + }, + "required": true, + "description": "The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ids": { + "minItems": 1, + "maxItems": 100, + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "example": "my-doc-123", + "description": "A document ID or caller-defined ID" + }, + "description": "Document identifiers to permanently delete from this namespace", + "example": [ + "my-doc-123" + ] + } + }, + "required": [ + "ids" + ], + "additionalProperties": false + } + } + } + } + } + }, + "/ns/{namespace}/document/batch": { + "post": { + "operationId": "postNsByNamespaceDocumentBatch", + "description": "Build a knowledge base efficiently by ingesting up to 600 text or URL documents at once. Existing caller-defined IDs append new information using the same semantics as single-document ingestion.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier of the document (empty string for failed items)" + }, + "status": { + "type": "string", + "description": "Status of the document (e.g. 'done', 'queued', 'error')" + }, + "error": { + "description": "Error message when status is 'error'", + "type": "string" + }, + "details": { + "description": "Additional error details when status is 'error'", + "type": "string" + }, + "url": { + "description": "URL of the failed item, when it had one. Only present on failed items, where it may be the only way to tell which input the error belongs to (the id falls back to 'unknown' when the input had neither an id nor a customId).", + "type": "string" + } + }, + "required": [ + "id", + "status" + ], + "additionalProperties": false + }, + "description": "Array of results for each document in the batch" + }, + "failed": { + "type": "number", + "description": "Count of documents that failed to add" + }, + "success": { + "type": "number", + "description": "Count of documents successfully added" + } + }, + "required": [ + "results", + "failed", + "success" + ], + "additionalProperties": false + } + } + }, + "description": "Batch accepted for processing" + }, + "400": { + "description": "Invalid request parameters", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "402": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Insufficient processing credits" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Namespace write access denied" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server error" + } + }, + "summary": "Batch add documents", + "tags": [ + "Ingest" + ], + "parameters": [ + { + "in": "path", + "name": "namespace", + "schema": { + "type": "string", + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_:-]+$", + "example": "user_alex" + }, + "required": true, + "description": "The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents." + }, + { + "in": "query", + "name": "taskType", + "schema": { + "type": "string", + "enum": [ + "memory", + "superrag" + ] + }, + "description": "Processing pipeline. \"memory\" builds durable learned context; \"superrag\" optimizes the document for retrieval without generating memories." + }, + { + "in": "query", + "name": "dreaming", + "schema": { + "type": "string", + "enum": [ + "dynamic", + "instant" + ] + }, + "description": "Processing mode. \"dynamic\" (default) groups related documents so memories form from coherent context. \"instant\" processes each document independently right away and bills one extra operation per document." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "documents": { + "minItems": 1, + "maxItems": 600, + "type": "array", + "items": { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "The content to process. This may be plaintext or a URL to supported rich content.", + "example": "Supermemory turns unstructured content into evolving memory." + }, + "id": { + "description": "An optional caller-defined document ID", + "type": "string", + "minLength": 1, + "maxLength": 255, + "example": "my-doc-123" + }, + "supportingContext": { + "description": "Context used to guide memory extraction within this namespace. Max 1500 characters.", + "example": "Focus on product decisions, dates, and owners.", + "type": "string", + "maxLength": 1500 + }, + "metadata": { + "description": "Arbitrary metadata attached to the document and derived data", + "example": { + "source": "api-docs" + }, + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + "group": { + "description": "Metadata values that isolate related context within the namespace", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + "date": { + "description": "When the source content is from, in ISO 8601 format", + "example": "2026-01-15", + "type": "string" + } + }, + "required": [ + "content" + ], + "additionalProperties": false + }, + "description": "Documents to ingest or append in one request" + } + }, + "required": [ + "documents" + ], + "additionalProperties": false + } + } + } + }, + "x-codeSamples": [ + { + "lang": "bash", + "label": "cURL", + "source": "curl -X POST \"https://api.supermemory.ai/ns/user_123/document/batch?dreaming=instant\" \\\n -H \"Authorization: Bearer $SUPERMEMORY_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"documents\": [\n { \"content\": \"Q3 roadmap notes\", \"id\": \"doc_roadmap\" },\n { \"content\": \"Gift policy for VP promotions\", \"id\": \"doc_gifts\" }\n ]\n }'" + } + ] + } + }, + "/ns/{namespace}/document/{id}": { + "get": { + "operationId": "getNsByNamespaceDocumentById", + "description": "Retrieve a document's canonical content, metadata, and processing state by document ID or caller-defined ID. Optionally attach its source chunks, derived memories, or both in the same response.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable document identifier", + "example": "doc_xyz789" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Extracted or caller-provided document title", + "example": "API Rate Limiting Policy" + }, + "type": { + "type": "string", + "description": "Detected source or content type", + "example": "text" + }, + "summary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Generated summary of the document, when available" + }, + "content": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Canonical extracted text used for processing and recall", + "example": "Our API rate limits are 100 req/min on free and 1000 on pro." + }, + "metadata": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {}, + "description": "Caller metadata with internal fields removed", + "example": { + "source": "api-docs" + } + }, + "system": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Current extraction and memory-processing status" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp when the document was created" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp of the latest document update" + } + }, + "required": [ + "status", + "createdAt", + "updatedAt" + ], + "description": "Processing status, lifecycle timestamps, and storage path maintained by Supermemory" + }, + "chunks": { + "description": "Ordered document chunks; included only when requested", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable chunk identifier" + }, + "position": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "description": "Zero-based order within the document" + }, + "content": { + "type": "string", + "description": "Chunk text used for retrieval" + }, + "type": { + "type": "string", + "description": "Chunk content type" + }, + "metadata": { + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ], + "description": "Chunk metadata, when available" + }, + "system": { + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp when the chunk was created" + } + }, + "required": [ + "createdAt" + ], + "description": "Lifecycle timestamps maintained by Supermemory" + } + }, + "required": [ + "id", + "position", + "content", + "type", + "metadata", + "system" + ], + "description": "A chunk of source content belonging to a document" + } + }, + "memories": { + "description": "Memories derived from the document; included only when requested", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable memory identifier" + }, + "memory": { + "type": "string", + "description": "Learned fact or context extracted from the document" + }, + "metadata": { + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ], + "description": "Memory metadata, including temporal context when available" + }, + "isStatic": { + "type": "boolean", + "description": "Whether the memory belongs to the stable profile section" + }, + "isInference": { + "type": "boolean", + "description": "Whether the memory was inferred rather than explicitly stated" + }, + "isLatest": { + "type": "boolean", + "description": "Whether this is the newest version in its memory chain" + }, + "isForgotten": { + "type": "boolean", + "description": "Whether this memory has been excluded from normal recall" + }, + "version": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "description": "Version number within the memory chain" + }, + "system": { + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp when this memory version was created" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp of the latest update to this version" + } + }, + "required": [ + "createdAt", + "updatedAt" + ], + "description": "Lifecycle timestamps maintained by Supermemory" + } + }, + "required": [ + "id", + "memory", + "metadata", + "isStatic", + "isInference", + "isLatest", + "isForgotten", + "version", + "system" + ], + "description": "A memory formed from documents in this namespace" + } + } + }, + "required": [ + "id", + "title", + "type", + "summary", + "content", + "metadata", + "system" + ], + "description": "Namespace-scoped document. Attachment keys are omitted unless requested and are empty arrays when requested without results." + } + } + }, + "description": "Document retrieved successfully" + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Namespace access denied" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Document not found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal server error" + } + }, + "summary": "Get document", + "tags": [ + "Content Management" + ], + "parameters": [ + { + "in": "path", + "name": "namespace", + "schema": { + "type": "string", + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_:-]+$", + "example": "user_alex" + }, + "required": true, + "description": "The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents." + }, + { + "in": "path", + "name": "id", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "example": "my-doc-123" + }, + "required": true, + "description": "The public document ID" + }, + { + "in": "query", + "name": "attach", + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "chunks", + "memories" + ] + } + }, + "description": "Child resources to include. Repeat the parameter to attach chunks, memories, or both." + } + ], + "x-codeSamples": [ + { + "lang": "bash", + "label": "cURL", + "source": "curl \"https://api.supermemory.ai/ns/project_123/document/doc_abc123?attach=chunks&attach=memories\" \\\n -H \"Authorization: Bearer $SUPERMEMORY_API_KEY\"" + } + ] + }, + "patch": { + "operationId": "patchNsByNamespaceDocumentById", + "description": "Refresh an existing document without changing its stable ID. Supplied content replaces the canonical content and is reprocessed; omitted fields remain unchanged.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier of the document" + }, + "status": { + "type": "string", + "description": "Status of the document" + } + }, + "required": [ + "id", + "status" + ], + "additionalProperties": false + } + } + }, + "description": "Document updated successfully" + }, + "400": { + "description": "Invalid request parameters", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "402": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Insufficient processing credits" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Namespace write access denied" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Document not found in this namespace" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Namespace merge or filepath conflict" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server error" + } + }, + "summary": "Update document", + "tags": [ + "Ingest" + ], + "parameters": [ + { + "in": "path", + "name": "namespace", + "schema": { + "type": "string", + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_:-]+$", + "example": "user_alex" + }, + "required": true, + "description": "The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents." + }, + { + "in": "path", + "name": "id", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "example": "my-doc-123" + }, + "required": true, + "description": "The public document ID" + }, + { + "in": "query", + "name": "taskType", + "schema": { + "type": "string", + "enum": [ + "memory", + "superrag" + ] + }, + "description": "Processing pipeline. \"memory\" builds durable learned context; \"superrag\" optimizes the document for retrieval without generating memories." + }, + { + "in": "query", + "name": "dreaming", + "schema": { + "type": "string", + "enum": [ + "dynamic", + "instant" + ] + }, + "description": "Processing mode. \"dynamic\" (default) groups related documents so memories form from coherent context. \"instant\" processes each document independently right away and bills one extra operation per document." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "The content to process. This may be plaintext or a URL to supported rich content.", + "example": "Supermemory turns unstructured content into evolving memory." + }, + "supportingContext": { + "description": "Context used to guide memory extraction within this namespace. Max 1500 characters.", + "example": "Focus on product decisions, dates, and owners.", + "type": "string", + "maxLength": 1500 + }, + "metadata": { + "description": "Arbitrary metadata attached to the document and derived data", + "example": { + "source": "api-docs" + }, + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + "group": { + "description": "Metadata values that isolate related context within the namespace", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + "date": { + "description": "When the source content is from, in ISO 8601 format", + "example": "2026-01-15", + "type": "string" + } + }, + "additionalProperties": false + } + } + } + }, + "x-codeSamples": [ + { + "lang": "bash", + "label": "cURL", + "source": "curl -X PATCH \"https://api.supermemory.ai/ns/user_123/document/chat_session_1\" \\\n -H \"Authorization: Bearer $SUPERMEMORY_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"content\": \"Updated notes \u2014 Sarah is now VP of Product.\",\n \"metadata\": { \"type\": \"conversation\" }\n }'" + } + ] + } + }, + "/ns/{namespace}/document/file": { + "post": { + "operationId": "postNsByNamespaceDocumentFile", + "description": "Transform an uploaded file into searchable knowledge and learned memory. The response returns as soon as ingestion is safely queued while extraction and memory formation continue asynchronously.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1, + "description": "Document identifier to poll or retrieve" + }, + "status": { + "type": "string", + "const": "processing", + "description": "Confirms the file was accepted for asynchronous processing" + } + }, + "required": [ + "id", + "status" + ], + "additionalProperties": false + } + } + }, + "description": "File accepted for processing" + }, + "400": { + "description": "Invalid file request", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "402": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Insufficient balance or document quota" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Write access required for this namespace" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Filepath conflict or namespace merge prevents the update" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal server error" + } + }, + "summary": "Upload file", + "tags": [ + "Ingest" + ], + "parameters": [ + { + "in": "path", + "name": "namespace", + "schema": { + "type": "string", + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_:-]+$", + "example": "user_alex" + }, + "required": true, + "description": "The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents." + }, + { + "in": "query", + "name": "taskType", + "schema": { + "type": "string", + "enum": [ + "memory", + "superrag" + ] + }, + "description": "Processing pipeline. \"memory\" builds durable learned context; \"superrag\" optimizes the document for retrieval without generating memories." + }, + { + "in": "query", + "name": "dreaming", + "schema": { + "type": "string", + "enum": [ + "dynamic", + "instant" + ] + }, + "description": "Processing mode. \"dynamic\" (default) groups related documents so memories form from coherent context. \"instant\" processes each document independently right away and bills one extra operation per document." + }, + { + "in": "query", + "name": "fileType", + "schema": { + "type": "string", + "enum": [ + "text", + "pdf", + "image", + "video", + "audio" + ] + }, + "description": "Explicit source type used when automatic inference is insufficient" + }, + { + "in": "query", + "name": "mimeType", + "schema": { + "type": "string", + "pattern": "^[^\\s/]+\\/[^\\s/]+$" + }, + "description": "Explicit MIME type used when upload metadata is insufficient" + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "description": "File contents to upload", + "format": "binary", + "type": "string" + }, + "supportingContext": { + "description": "Context used to guide memory extraction within this namespace. Max 1500 characters.", + "example": "Focus on product decisions, dates, and owners.", + "type": "string", + "maxLength": 1500 + }, + "metadata": { + "description": "JSON-encoded metadata to attach to the document and its derived memories", + "type": "string" + }, + "group": { + "description": "JSON-encoded metadata used to keep related context isolated during processing and recall", + "type": "string" + }, + "date": { + "description": "When the source content is from, in ISO 8601 format", + "example": "2026-01-15", + "type": "string" + } + }, + "required": [ + "file" + ], + "additionalProperties": false + } + } + } + } + } + }, + "/ns/{namespace}/document/file/{id}": { + "post": { + "operationId": "postNsByNamespaceDocumentFileById", + "description": "Replace an existing document with a new file while keeping its stable document ID. Content and caller metadata are overwritten, then the document is reprocessed asynchronously.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1, + "description": "Document identifier to poll or retrieve" + }, + "status": { + "type": "string", + "const": "processing", + "description": "Confirms the file was accepted for asynchronous processing" + } + }, + "required": [ + "id", + "status" + ], + "additionalProperties": false + } + } + }, + "description": "Replacement accepted for processing" + }, + "400": { + "description": "Invalid file request", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "402": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Insufficient balance or document quota" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Write access required for this namespace" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Document not found in this namespace" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Filepath conflict or namespace merge prevents the update" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal server error" + } + }, + "summary": "Replace document with file", + "tags": [ + "Ingest" + ], + "parameters": [ + { + "in": "path", + "name": "namespace", + "schema": { + "type": "string", + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_:-]+$", + "example": "user_alex" + }, + "required": true, + "description": "The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents." + }, + { + "in": "path", + "name": "id", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "example": "my-doc-123" + }, + "required": true, + "description": "The public document ID" + }, + { + "in": "query", + "name": "taskType", + "schema": { + "type": "string", + "enum": [ + "memory", + "superrag" + ] + }, + "description": "Processing pipeline. \"memory\" builds durable learned context; \"superrag\" optimizes the document for retrieval without generating memories." + }, + { + "in": "query", + "name": "dreaming", + "schema": { + "type": "string", + "enum": [ + "dynamic", + "instant" + ] + }, + "description": "Processing mode. \"dynamic\" (default) groups related documents so memories form from coherent context. \"instant\" processes each document independently right away and bills one extra operation per document." + }, + { + "in": "query", + "name": "fileType", + "schema": { + "type": "string", + "enum": [ + "text", + "pdf", + "image", + "video", + "audio" + ] + }, + "description": "Explicit source type used when automatic inference is insufficient" + }, + { + "in": "query", + "name": "mimeType", + "schema": { + "type": "string", + "pattern": "^[^\\s/]+\\/[^\\s/]+$" + }, + "description": "Explicit MIME type used when upload metadata is insufficient" + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "description": "File contents to upload", + "format": "binary", + "type": "string" + }, + "supportingContext": { + "description": "Context used to guide memory extraction within this namespace. Max 1500 characters.", + "example": "Focus on product decisions, dates, and owners.", + "type": "string", + "maxLength": 1500 + }, + "metadata": { + "description": "JSON-encoded metadata to attach to the document and its derived memories", + "type": "string" + }, + "group": { + "description": "JSON-encoded metadata used to keep related context isolated during processing and recall", + "type": "string" + }, + "date": { + "description": "When the source content is from, in ISO 8601 format", + "example": "2026-01-15", + "type": "string" + } + }, + "required": [ + "file" + ], + "additionalProperties": false + } + } + } + } + }, + "patch": { + "operationId": "patchNsByNamespaceDocumentFileById", + "description": "Refresh only the file-backed fields you provide. Supplying a file replaces the canonical content; omitted metadata and processing context remain unchanged.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1, + "description": "Document identifier to poll or retrieve" + }, + "status": { + "type": "string", + "const": "processing", + "description": "Confirms the file was accepted for asynchronous processing" + } + }, + "required": [ + "id", + "status" + ], + "additionalProperties": false + } + } + }, + "description": "Update accepted for processing" + }, + "400": { + "description": "Invalid file request", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "402": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Insufficient balance or document quota" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Write access required for this namespace" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Document not found in this namespace" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Filepath conflict or namespace merge prevents the update" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal server error" + } + }, + "summary": "Partially update document file", + "tags": [ + "Ingest" + ], + "parameters": [ + { + "in": "path", + "name": "namespace", + "schema": { + "type": "string", + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_:-]+$", + "example": "user_alex" + }, + "required": true, + "description": "The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents." + }, + { + "in": "path", + "name": "id", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "example": "my-doc-123" + }, + "required": true, + "description": "The public document ID" + }, + { + "in": "query", + "name": "taskType", + "schema": { + "type": "string", + "enum": [ + "memory", + "superrag" + ] + }, + "description": "Processing pipeline. \"memory\" builds durable learned context; \"superrag\" optimizes the document for retrieval without generating memories." + }, + { + "in": "query", + "name": "dreaming", + "schema": { + "type": "string", + "enum": [ + "dynamic", + "instant" + ] + }, + "description": "Processing mode. \"dynamic\" (default) groups related documents so memories form from coherent context. \"instant\" processes each document independently right away and bills one extra operation per document." + }, + { + "in": "query", + "name": "fileType", + "schema": { + "type": "string", + "enum": [ + "text", + "pdf", + "image", + "video", + "audio" + ] + }, + "description": "Explicit source type used when automatic inference is insufficient" + }, + { + "in": "query", + "name": "mimeType", + "schema": { + "type": "string", + "pattern": "^[^\\s/]+\\/[^\\s/]+$" + }, + "description": "Explicit MIME type used when upload metadata is insufficient" + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "description": "File contents to upload", + "format": "binary", + "type": "string" + }, + "supportingContext": { + "description": "Context used to guide memory extraction within this namespace. Max 1500 characters.", + "example": "Focus on product decisions, dates, and owners.", + "type": "string", + "maxLength": 1500 + }, + "metadata": { + "description": "JSON-encoded metadata to attach to the document and its derived memories", + "type": "string" + }, + "group": { + "description": "JSON-encoded metadata used to keep related context isolated during processing and recall", + "type": "string" + }, + "date": { + "description": "When the source content is from, in ISO 8601 format", + "example": "2026-01-15", + "type": "string" + } + }, + "additionalProperties": false + } + } + } + } + } + }, + "/ns/{namespace}/search": { + "post": { + "operationId": "postNsByNamespaceSearch", + "description": "Recall the most relevant learned context and source passages from a namespace. Hybrid search combines memories with document chunks by default, with optional query rewriting, reranking, and supporting context attachments.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable identifier for the matching result", + "example": "mem_abc123" + }, + "memory": { + "description": "Learned fact or context returned by memory search (only present for memory results)", + "example": "The user prefers detailed API responses over minimal ones.", + "type": "string" + }, + "chunk": { + "description": "Source passage returned by chunk search (only present for chunk results from hybrid search)", + "example": "This is a chunk of content from a document...", + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ], + "description": "Public metadata attached to the result", + "example": { + "source": "conversation", + "confidence": 0.9 + } + }, + "similarity": { + "type": "number", + "description": "Normalized relevance score used to rank the result, from 0 to 1", + "example": 0.89 + }, + "isLatest": { + "type": "boolean", + "description": "Whether the memory is its latest version; false for recalled forgotten memories" + }, + "system": { + "type": "object", + "properties": { + "updatedAt": { + "type": "string", + "description": "ISO 8601 timestamp of the latest result update" + }, + "createdAt": { + "description": "ISO 8601 timestamp when the result was first created", + "type": "string" + } + }, + "required": [ + "updatedAt" + ], + "description": "Lifecycle and filesystem details for the result" + }, + "included": { + "description": "Requested supporting context for the result", + "type": "object", + "properties": { + "related": { + "description": "Memory relationships attached when requested", + "type": "object", + "properties": { + "parents": { + "type": "array", + "items": { + "type": "object", + "properties": { + "relation": { + "type": "string", + "enum": [ + "updates", + "extends", + "derives" + ], + "description": "How this memory is connected to the matched memory" + }, + "version": { + "description": "Version number within the related memory's history", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "memory": { + "type": "string", + "description": "Related learned fact or context" + }, + "metadata": { + "description": "Public metadata associated with the related memory", + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] + }, + "system": { + "type": "object", + "properties": { + "updatedAt": { + "type": "string", + "description": "ISO 8601 timestamp of the related memory's latest update" + } + }, + "required": [ + "updatedAt" + ], + "description": "Lifecycle details for the related memory" + } + }, + "required": [ + "relation", + "memory", + "system" + ] + }, + "description": "Earlier memories this result updates or derives from" + }, + "children": { + "type": "array", + "items": { + "type": "object", + "properties": { + "relation": { + "type": "string", + "enum": [ + "updates", + "extends", + "derives" + ], + "description": "How this memory is connected to the matched memory" + }, + "version": { + "description": "Version number within the related memory's history", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "memory": { + "type": "string", + "description": "Related learned fact or context" + }, + "metadata": { + "description": "Public metadata associated with the related memory", + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] + }, + "system": { + "type": "object", + "properties": { + "updatedAt": { + "type": "string", + "description": "ISO 8601 timestamp of the related memory's latest update" + } + }, + "required": [ + "updatedAt" + ], + "description": "Lifecycle details for the related memory" + } + }, + "required": [ + "relation", + "memory", + "system" + ] + }, + "description": "Newer memories that update or extend this result" + }, + "siblings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "relation": { + "type": "string", + "enum": [ + "updates", + "extends", + "derives" + ], + "description": "How this memory is connected to the matched memory" + }, + "version": { + "description": "Version number within the related memory's history", + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "memory": { + "type": "string", + "description": "Related learned fact or context" + }, + "metadata": { + "description": "Public metadata associated with the related memory", + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] + }, + "system": { + "type": "object", + "properties": { + "updatedAt": { + "type": "string", + "description": "ISO 8601 timestamp of the related memory's latest update" + } + }, + "required": [ + "updatedAt" + ], + "description": "Lifecycle details for the related memory" + } + }, + "required": [ + "relation", + "memory", + "system" + ] + }, + "description": "Other memories derived from related context" + } + }, + "required": [ + "parents", + "children", + "siblings" + ] + }, + "document": { + "description": "Source document attached when requested and available", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Source document identifier" + }, + "title": { + "description": "Source document title", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "type": { + "description": "Detected source or content type", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "metadata": { + "description": "Public metadata attached to the source document", + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] + }, + "summary": { + "description": "Generated summary of the source document", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string", + "description": "ISO 8601 timestamp when the source document was created" + }, + "updatedAt": { + "type": "string", + "description": "ISO 8601 timestamp of the source document's latest update" + } + }, + "required": [ + "id", + "createdAt", + "updatedAt" + ] + } + } + } + }, + "required": [ + "id", + "metadata", + "similarity", + "isLatest", + "system" + ] + }, + "description": "Ranked memories and source chunks matching the query" + }, + "searchTime": { + "type": "number", + "description": "Server-side search duration in milliseconds" + } + }, + "required": [ + "results", + "searchTime" + ] + } + } + }, + "description": "Search results" + }, + "400": { + "description": "Invalid request parameters", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "402": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Search quota or credits exhausted" + }, + "403": { + "description": "The API key cannot access this endpoint or namespace", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server error" + } + }, + "summary": "Search memories", + "tags": [ + "Recall (Search)" + ], + "parameters": [ + { + "in": "path", + "name": "namespace", + "schema": { + "type": "string", + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_:-]+$", + "example": "user_alex" + }, + "required": true, + "description": "The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories." + }, + { + "in": "query", + "name": "limit", + "schema": { + "default": 10, + "example": 10, + "type": "integer", + "minimum": 1, + "maximum": 100 + }, + "description": "Maximum number of results to return" + }, + { + "in": "query", + "name": "searchMode", + "schema": { + "default": "hybrid", + "example": "hybrid", + "type": "string", + "enum": [ + "hybrid", + "memories", + "chunks" + ] + }, + "description": "Search surface. \"hybrid\" combines learned memories with source chunks, \"memories\" returns learned context, and \"chunks\" returns source passages." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Natural-language question, topic, or phrase to retrieve relevant context for. Replaces the v4 `q` field.", + "example": "what are the API rate limits" + }, + "filter": { + "description": "Type-safe metadata conditions applied before ranking results. Replaces the v4 `filters` field and unifies both legacy filter formats into one expression type.", + "example": { + "field": "source", + "operator": "eq", + "value": "api-docs" + }, + "$ref": "#/components/schemas/FilterExpression" + }, + "attach": { + "default": { + "documents": false, + "related": false, + "forgotten": false + }, + "description": "Optional context to include alongside each matching result", + "type": "object", + "properties": { + "documents": { + "default": false, + "description": "Attach the source document for each result when one is available", + "type": "boolean" + }, + "related": { + "default": false, + "description": "Attach parent, child, and sibling memories that explain how each memory evolved", + "type": "boolean" + }, + "forgotten": { + "default": false, + "description": "Allow forgotten memories to participate in recall", + "type": "boolean" + } + }, + "additionalProperties": false + }, + "threshold": { + "default": 0.3, + "description": "Minimum relevance score from 0 to 1. Raise it for precision (fewer, accurate results) or lower it for broader recall (more results).", + "example": 0.5, + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "rerank": { + "default": "none", + "description": "Post-retrieval ranking. \"order\" improves result ordering; \"aggregate\" also combines overlapping context into cleaner answers. This is helpful if you want to ensure the most relevant results are returned.", + "example": "order", + "type": "string", + "enum": [ + "none", + "order", + "aggregate" + ] + }, + "rewriteQuery": { + "default": false, + "description": "Expand and clarify the query before retrieval to improve recall for conversational or underspecified prompts. This increases the latency by about 400ms.", + "example": false, + "type": "boolean" + } + }, + "required": [ + "query" + ], + "additionalProperties": false + } + } + } + }, + "x-codeSamples": [ + { + "lang": "bash", + "label": "cURL", + "source": "curl -X POST \"https://api.supermemory.ai/ns/user_123/search?searchMode=hybrid&limit=5\" \\\n -H \"Authorization: Bearer $SUPERMEMORY_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"query\": \"gift ideas for a VP promotion after a Tokyo offsite\",\n \"filter\": {\n \"operator\": \"and\",\n \"operands\": [\n { \"field\": \"category\", \"operator\": \"eq\", \"value\": \"work\" },\n { \"field\": \"importance\", \"operator\": \"gte\", \"value\": 0.8 }\n ]\n },\n \"attach\": { \"related\": true }\n }'" + } + ] + } + }, + "/ns/{namespace}/profile": { + "post": { + "operationId": "postNsByNamespaceProfile", + "description": "Read a continuously maintained understanding of the subject represented by this namespace. Stable facts, evolving context, and selected custom buckets are returned together without requiring a search query.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "profile": { + "type": "object", + "properties": { + "static": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Memory ID, or an aggregated_-prefixed ID for a summary synthesized from several memories" + }, + "memory": { + "type": "string", + "description": "Memory text" + } + }, + "required": [ + "id", + "memory" + ] + }, + "description": "Durable facts that define the subject and rarely change, such as identity or long-term preferences" + }, + "dynamic": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Memory ID, or an aggregated_-prefixed ID for a summary synthesized from several memories" + }, + "memory": { + "type": "string", + "description": "Memory text" + } + }, + "required": [ + "id", + "memory" + ] + }, + "description": "Recent or evolving context, such as current goals, projects, and active interests" + }, + "buckets": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Memory ID, or an aggregated_-prefixed ID for a summary synthesized from several memories" + }, + "memory": { + "type": "string", + "description": "Memory text" + } + }, + "required": [ + "id", + "memory" + ] + } + }, + "description": "Custom profile sections keyed by bucket name, with the memories currently assigned to each bucket" + } + }, + "required": [ + "static", + "dynamic", + "buckets" + ], + "description": "Continuously maintained understanding of the subject represented by this namespace" + } + }, + "required": [ + "profile" + ] + } + }, + "text/markdown": { + "schema": { + "type": "string" + } + } + }, + "description": "Namespace profile. Send `Accept: text/markdown` to receive it as a markdown document instead of JSON." + }, + "400": { + "description": "Invalid request parameters", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "description": "The API key cannot access this endpoint or namespace", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server error" + } + }, + "summary": "Get profile", + "tags": [ + "Profiles" + ], + "parameters": [ + { + "in": "path", + "name": "namespace", + "schema": { + "type": "string", + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_:-]+$", + "example": "user_alex" + }, + "required": true, + "description": "The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories." + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "filter": { + "description": "Type-safe metadata conditions that limit which memories contribute to the profile", + "$ref": "#/components/schemas/FilterExpression" + }, + "buckets": { + "description": "Custom buckets to return. Omit to return every effective bucket.", + "example": [ + "interests", + "goals" + ], + "maxItems": 50, + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[a-z0-9][a-z0-9_-]*$", + "description": "Stable slug for the bucket, stored on each memory" + } + } + }, + "additionalProperties": false + } + } + } + }, + "x-codeSamples": [ + { + "lang": "bash", + "label": "cURL", + "source": "curl -X POST \"https://api.supermemory.ai/ns/user_123/profile\" \\\n -H \"Authorization: Bearer $SUPERMEMORY_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"filter\": {\n \"field\": \"region\",\n \"operator\": \"eq\",\n \"value\": \"us-west\"\n }\n }'" + } + ] + } + }, + "/ns/{namespace}/profile/buckets": { + "get": { + "operationId": "getNsByNamespaceProfileBuckets", + "description": "Inspect the profile taxonomy active in this namespace. The response combines organization-wide buckets with namespace-owned additions as a concise name-to-description map.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "buckets": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + }, + "description": "Effective bucket definitions keyed by name. Includes organization buckets and namespace-owned additions." + } + }, + "required": [ + "buckets" + ] + } + } + }, + "description": "Effective namespace profile bucket definitions" + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "description": "The API key cannot access this endpoint or namespace", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "summary": "Get profile buckets", + "tags": [ + "Profiles" + ], + "parameters": [ + { + "in": "path", + "name": "namespace", + "schema": { + "type": "string", + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_:-]+$", + "example": "user_alex" + }, + "required": true, + "description": "The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories." + } + ], + "x-codeSamples": [ + { + "lang": "bash", + "label": "cURL", + "source": "curl \"https://api.supermemory.ai/ns/user_123/profile/buckets\" \\\n -H \"Authorization: Bearer $SUPERMEMORY_API_KEY\"" + }, + { + "lang": "typescript", + "label": "TypeScript SDK", + "source": "import Supermemory from \"supermemory\";\n\nconst client = new Supermemory({\n apiKey: process.env.SUPERMEMORY_API_KEY,\n});\n\nconst res = await fetch(\"https://api.supermemory.ai/ns/user_123/profile/buckets\", {\n headers: {\n Authorization: `Bearer ${process.env.SUPERMEMORY_API_KEY}`,\n },\n});\nconst data = await res.json();\nconsole.log(data);" + } + ] + }, + "put": { + "operationId": "putNsByNamespaceProfileBuckets", + "description": "Teach Supermemory new ways to organize this namespace's profile, or refine how existing namespace-owned buckets are classified. Omitted buckets remain unchanged and organization-wide buckets stay protected.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "buckets": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + }, + "description": "Effective bucket definitions keyed by name. Includes organization buckets and namespace-owned additions." + } + }, + "required": [ + "buckets" + ] + } + } + }, + "description": "Updated effective namespace profile bucket definitions" + }, + "400": { + "description": "Invalid profile bucket definitions", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Namespace write access required" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Namespace not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "An organization-level bucket cannot be changed" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal server error" + } + }, + "summary": "Add or update profile buckets", + "tags": [ + "Profiles" + ], + "parameters": [ + { + "in": "path", + "name": "namespace", + "schema": { + "type": "string", + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_:-]+$", + "example": "user_alex" + }, + "required": true, + "description": "The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "buckets": { + "type": "object", + "propertyNames": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[a-z0-9][a-z0-9_-]*$", + "description": "Stable slug for the bucket, stored on each memory" + }, + "additionalProperties": { + "type": "string", + "maxLength": 2000 + }, + "description": "Namespace-owned bucket names mapped to descriptions that guide memory classification. Existing names are updated; new names are added.", + "example": { + "interests": "Topics the subject actively follows" + } + } + }, + "required": [ + "buckets" + ], + "additionalProperties": false + } + } + } + } + }, + "delete": { + "operationId": "deleteNsByNamespaceProfileBuckets", + "description": "Remove profile categories created specifically for this namespace. Unknown names are safely ignored, while organization-wide buckets remain protected.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "buckets": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + }, + "description": "Effective bucket definitions keyed by name. Includes organization buckets and namespace-owned additions." + } + }, + "required": [ + "buckets" + ] + } + } + }, + "description": "Updated effective namespace profile bucket definitions" + }, + "400": { + "description": "Invalid profile bucket names", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Namespace write access required" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Namespace not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "An organization-level bucket cannot be deleted" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal server error" + } + }, + "summary": "Delete profile buckets", + "tags": [ + "Profiles" + ], + "parameters": [ + { + "in": "path", + "name": "namespace", + "schema": { + "type": "string", + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_:-]+$", + "example": "user_alex" + }, + "required": true, + "description": "The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "buckets": { + "minItems": 1, + "maxItems": 50, + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[a-z0-9][a-z0-9_-]*$", + "description": "Stable slug for the bucket, stored on each memory" + }, + "description": "Namespace-owned bucket names to delete. Organization-level buckets are protected and cannot be removed here.", + "example": [ + "interests" + ] + } + }, + "required": [ + "buckets" + ], + "additionalProperties": false + } + } + } + } + } + }, + "/ns/{namespace}/memories": { + "delete": { + "operationId": "deleteNsByNamespaceMemories", + "description": "Remove exact memories from normal recall while preserving their audit history. Each ID is handled independently and any missing or ineligible memory is reported without rolling back successful changes.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991, + "description": "Number of memories matched by this operation" + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Requested memory identifier" + }, + "error": { + "type": "string", + "description": "Why the memory could not be forgotten" + } + }, + "required": [ + "id", + "error" + ], + "additionalProperties": false + }, + "description": "Per-memory failures for exact-ID requests" + }, + "matches": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable identifier of the matched memory" + }, + "memory": { + "type": "string", + "description": "Matched memory text" + } + }, + "required": [ + "id", + "memory" + ], + "additionalProperties": false + }, + "description": "Memories selected by the operation, whether previewed or forgotten" + } + }, + "required": [ + "count", + "errors", + "matches" + ], + "additionalProperties": false + } + } + }, + "description": "Exact memory forget completed" + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Namespace write access denied" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server error" + } + }, + "summary": "Forget memories by ID", + "tags": [ + "Content Management" + ], + "parameters": [ + { + "in": "path", + "name": "namespace", + "schema": { + "type": "string", + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_:-]+$", + "example": "user_alex" + }, + "required": true, + "description": "Namespace containing the memories to forget. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ids": { + "minItems": 1, + "maxItems": 500, + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "description": "Stable memory identifier" + }, + "description": "Memory identifiers to remove from normal recall", + "example": [ + "mem_abc123" + ] + } + }, + "required": [ + "ids" + ], + "additionalProperties": false + } + } + } + }, + "x-codeSamples": [ + { + "lang": "bash", + "label": "cURL", + "source": "curl -X DELETE \"https://api.supermemory.ai/ns/user_123/memories\" \\\n -H \"Authorization: Bearer $SUPERMEMORY_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{ \"ids\": [\"mem_1\", \"mem_2\"] }'" + } + ] + } + }, + "/ns/{namespace}/memories/semantic": { + "delete": { + "operationId": "deleteNsByNamespaceMemoriesSemantic", + "description": "Describe what should be forgotten in natural language, then preview or apply the matching set. Use dry-run results with the exact-ID endpoint when you need a reviewable, drift-free workflow.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991, + "description": "Number of memories matched by this operation" + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Requested memory identifier" + }, + "error": { + "type": "string", + "description": "Why the memory could not be forgotten" + } + }, + "required": [ + "id", + "error" + ], + "additionalProperties": false + }, + "description": "Per-memory failures for exact-ID requests" + }, + "matches": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable identifier of the matched memory" + }, + "memory": { + "type": "string", + "description": "Matched memory text" + } + }, + "required": [ + "id", + "memory" + ], + "additionalProperties": false + }, + "description": "Memories selected by the operation, whether previewed or forgotten" + } + }, + "required": [ + "count", + "errors", + "matches" + ], + "additionalProperties": false + } + } + }, + "description": "Semantic memory forget completed or previewed" + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Namespace write access denied" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Server error" + } + }, + "summary": "Forget memories semantically", + "tags": [ + "Content Management" + ], + "parameters": [ + { + "in": "path", + "name": "namespace", + "schema": { + "type": "string", + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_:-]+$", + "example": "user_alex" + }, + "required": true, + "description": "Namespace containing the memories to forget. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "maxLength": 2000, + "description": "Natural-language description of the memories that should be forgotten", + "example": "everything about the old pricing plans" + }, + "dryRun": { + "type": "boolean", + "description": "When true, preview matching memories without changing their recall state", + "example": true + } + }, + "required": [ + "query", + "dryRun" + ], + "additionalProperties": false + } + } + } + }, + "x-codeSamples": [ + { + "lang": "bash", + "label": "cURL", + "source": "curl -X DELETE \"https://api.supermemory.ai/ns/user_123/memories/semantic\" \\\n -H \"Authorization: Bearer $SUPERMEMORY_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"query\": \"forget everything about the old pricing plan\",\n \"dryRun\": true\n }'" + } + ] + } + }, + "/ns/{namespace}/list/{type}": { + "post": { + "operationId": "postNsByNamespaceListByType", + "parameters": [ + { + "in": "path", + "name": "namespace", + "schema": { + "type": "string", + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_:-]+$", + "example": "user_alex" + }, + "required": true, + "description": "Namespace containing the resources to list. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope resources." + }, + { + "in": "path", + "name": "type", + "schema": { + "type": "string", + "enum": [ + "documents", + "chunks", + "memories" + ], + "example": "memories" + }, + "required": true, + "description": "Resource collection to list: source documents, extracted chunks, or learned memories" + }, + { + "in": "query", + "name": "page", + "schema": { + "example": 1, + "anyOf": [ + { + "type": "string", + "pattern": "^\\d+$" + }, + { + "type": "number" + } + ] + }, + "description": "One-based page number" + }, + { + "in": "query", + "name": "limit", + "schema": { + "example": 10, + "anyOf": [ + { + "type": "string", + "pattern": "^\\d+$" + }, + { + "type": "number" + } + ] + }, + "description": "Maximum resources to return per page" + }, + { + "in": "query", + "name": "sort", + "schema": { + "example": "createdAt", + "type": "string", + "enum": [ + "createdAt", + "updatedAt", + "position" + ] + }, + "description": "Field used to order results. Position is available only when listing chunks." + }, + { + "in": "query", + "name": "order", + "schema": { + "default": "desc", + "example": "desc", + "type": "string", + "enum": [ + "asc", + "desc" + ] + }, + "description": "Ascending or descending sort direction" + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "filter": { + "description": "Type-safe metadata conditions applied before pagination", + "$ref": "#/components/schemas/FilterExpression" + } + }, + "additionalProperties": false + } + } + } + }, + "description": "Browse documents, source chunks, or learned memories through one predictable paginated contract. Choose the collection in the path; the other collection arrays remain empty for a stable response shape.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "documents": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable document identifier" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Extracted or caller-provided document title" + }, + "type": { + "type": "string", + "description": "Detected source or content type" + }, + "summary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Generated summary of the document, when available" + }, + "metadata": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {}, + "description": "Public metadata attached to the document" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Original source URL, when the document was ingested from the web" + }, + "system": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Current extraction and memory-processing status" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp when the document was created" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp of the latest document update" + } + }, + "required": [ + "status", + "createdAt", + "updatedAt" + ], + "description": "Processing status, lifecycle timestamps, and storage path maintained by Supermemory" + } + }, + "required": [ + "id", + "title", + "type", + "summary", + "metadata", + "url", + "system" + ], + "description": "A document in this namespace without its content" + }, + "description": "Documents on this page; empty when a different resource type is requested" + }, + "chunks": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable chunk identifier" + }, + "position": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "description": "Zero-based order within the document" + }, + "content": { + "type": "string", + "description": "Chunk text used for retrieval" + }, + "type": { + "type": "string", + "description": "Chunk content type" + }, + "metadata": { + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ], + "description": "Chunk metadata, when available" + }, + "system": { + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp when the chunk was created" + } + }, + "required": [ + "createdAt" + ], + "description": "Lifecycle timestamps maintained by Supermemory" + }, + "documentId": { + "type": "string", + "description": "ID of the chunk's parent document" + } + }, + "required": [ + "id", + "position", + "content", + "type", + "metadata", + "system", + "documentId" + ] + }, + "description": "Chunks on this page; empty when a different resource type is requested" + }, + "memories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable memory identifier" + }, + "memory": { + "type": "string", + "description": "Learned fact or context extracted from the document" + }, + "metadata": { + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ], + "description": "Memory metadata, including temporal context when available" + }, + "isStatic": { + "type": "boolean", + "description": "Whether the memory belongs to the stable profile section" + }, + "isInference": { + "type": "boolean", + "description": "Whether the memory was inferred rather than explicitly stated" + }, + "isLatest": { + "type": "boolean", + "description": "Whether this is the newest version in its memory chain" + }, + "isForgotten": { + "type": "boolean", + "description": "Whether this memory has been excluded from normal recall" + }, + "version": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "description": "Version number within the memory chain" + }, + "system": { + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp when this memory version was created" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp of the latest update to this version" + } + }, + "required": [ + "createdAt", + "updatedAt" + ], + "description": "Lifecycle timestamps maintained by Supermemory" + } + }, + "required": [ + "id", + "memory", + "metadata", + "isStatic", + "isInference", + "isLatest", + "isForgotten", + "version", + "system" + ], + "description": "A memory formed from documents in this namespace" + }, + "description": "Memories on this page; empty when a different resource type is requested" + }, + "pagination": { + "type": "object", + "properties": { + "currentPage": { + "type": "number", + "description": "Current one-based page number" + }, + "limit": { + "default": 10, + "description": "Maximum resources returned per page", + "type": "number", + "maximum": 1100 + }, + "totalItems": { + "type": "number", + "description": "Total resources matching the request" + }, + "totalPages": { + "type": "number", + "description": "Total pages available at the selected limit" + } + }, + "required": [ + "currentPage", + "totalItems", + "totalPages" + ], + "description": "Page metadata for the selected resource collection" + } + }, + "required": [ + "documents", + "chunks", + "memories", + "pagination" + ], + "description": "A namespace-scoped page of one resource type" + } + } + }, + "description": "Namespace resources retrieved successfully" + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Namespace access denied" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal server error" + } + }, + "summary": "List resources", + "tags": [ + "Content Management" + ] + } + }, + "/connectors": { + "get": { + "operationId": "getConnectors", + "parameters": [ + { + "in": "query", + "name": "provider", + "schema": { + "type": "string", + "enum": [ + "notion", + "google-drive", + "onedrive", + "gmail", + "github", + "web-crawler", + "s3", + "granola" + ] + }, + "description": "Only return connectors for this provider" + }, + { + "in": "query", + "name": "page", + "schema": { + "default": 1, + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "description": "One-based page number" + }, + { + "in": "query", + "name": "limit", + "schema": { + "default": 50, + "type": "integer", + "minimum": 1, + "maximum": 100 + }, + "description": "Maximum connectors to return per page" + } + ], + "description": "List connectors across every namespace this key can read. Use it for admin views that span users or projects.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connectors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "provider": { + "type": "string", + "enum": [ + "notion", + "google-drive", + "onedrive", + "gmail", + "github", + "web-crawler", + "s3", + "granola" + ], + "description": "External source" + }, + "status": { + "type": "string", + "enum": [ + "pending_authorization", + "active" + ], + "description": "pending_authorization until the user finishes the provider login at authUrl; DELETE cancels it. Pending connectors are not listed." + }, + "authUrl": { + "description": "Present while pending, when the caller can create connectors in this namespace: where to send the user to authorize", + "type": "string" + }, + "authUrlExpiresAt": { + "description": "Present while pending: when authUrl stops working", + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "namespace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Namespace the connector syncs into. Null only for connectors created before namespaces were required." + }, + "account": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Account on the provider side, usually an email" + }, + "capabilities": { + "type": "object", + "properties": { + "selectable": { + "type": "boolean", + "description": "Supports choosing what to sync via the hosted picker" + }, + "webhooks": { + "type": "boolean", + "description": "Receives change notifications instead of only polling" + } + }, + "required": [ + "selectable", + "webhooks" + ] + }, + "config": { + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + { + "type": "null" + } + ], + "description": "Non-secret setup, such as the S3 bucket, crawler start URL or Notion workspace. Credentials are never returned." + }, + "selection": { + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + } + } + }, + { + "type": "null" + } + ], + "description": "What syncs, grouped by kind, in the same shape PATCH takes. Null for connectors that sync everything.", + "example": { + "labels": [ + { + "id": "INBOX", + "name": "Inbox" + } + ] + } + }, + "documentLimit": { + "type": "number" + }, + "documentCount": { + "type": "number" + }, + "lastSync": { + "anyOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "running", + "completed", + "failed" + ] + }, + "errorCode": { + "anyOf": [ + { + "type": "string", + "enum": [ + "auth_expired", + "rate_limited", + "plan_required", + "provider_unavailable", + "internal" + ] + }, + { + "type": "null" + } + ] + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Customer-safe error message" + }, + "startedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "completedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "status", + "errorCode", + "error", + "startedAt", + "completedAt" + ] + }, + { + "type": "null" + } + ], + "description": "Most recent sync run" + }, + "lastSyncedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "When content last finished syncing" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "syncs": { + "description": "Present when attach=syncs", + "type": "array", + "items": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "running", + "completed", + "failed" + ] + }, + "errorCode": { + "anyOf": [ + { + "type": "string", + "enum": [ + "auth_expired", + "rate_limited", + "plan_required", + "provider_unavailable", + "internal" + ] + }, + { + "type": "null" + } + ] + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Customer-safe error message" + }, + "startedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "completedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" + }, + "trigger": { + "type": "string", + "enum": [ + "event", + "cron", + "manual" + ] + }, + "itemsProcessed": { + "type": "number" + }, + "itemsFailed": { + "type": "number" + }, + "failures": { + "type": "array", + "items": { + "type": "object", + "properties": { + "documentId": { + "type": "string" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "errorCode": { + "type": "string" + }, + "errorMessage": { + "type": "string" + }, + "failedAt": { + "type": "string" + } + }, + "required": [ + "documentId", + "title", + "errorCode", + "errorMessage", + "failedAt" + ] + }, + "description": "Failed items, capped. itemsFailed is the true count." + } + }, + "required": [ + "status", + "errorCode", + "error", + "startedAt", + "completedAt", + "id", + "trigger", + "itemsProcessed", + "itemsFailed", + "failures" + ] + } + }, + "picker": { + "description": "Present when attach=picker. Null for connectors that sync everything.", + "anyOf": [ + { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "Send the user here to choose what syncs. Works once, in any browser." + }, + "expiresAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + } + }, + "required": [ + "url", + "expiresAt" + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "provider", + "status", + "namespace", + "account", + "capabilities", + "config", + "selection", + "documentLimit", + "documentCount", + "lastSync", + "lastSyncedAt", + "createdAt" + ] + } + }, + "pagination": { + "type": "object", + "properties": { + "currentPage": { + "type": "number" + }, + "limit": { + "default": 10, + "type": "number", + "maximum": 1100 + }, + "totalItems": { + "type": "number" + }, + "totalPages": { + "type": "number" + } + }, + "required": [ + "currentPage", + "totalItems", + "totalPages" + ], + "description": "Pagination metadata", + "example": { + "currentPage": 1, + "limit": 10, + "totalItems": 100, + "totalPages": 10 + } + } + }, + "required": [ + "connectors", + "pagination" + ] + } + } + }, + "description": "Connectors across namespaces" + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Namespace access denied" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal server error" + } + }, + "summary": "List all connectors", + "tags": [ + "Connectors" + ], + "x-speakeasy-group": "connectors", + "x-speakeasy-name-override": "listAll" + } + }, + "/ns/{namespace}/connectors": { + "post": { + "operationId": "postNsByNamespaceConnectors", + "parameters": [ + { + "in": "path", + "name": "namespace", + "schema": { + "type": "string", + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_:-]+$", + "example": "user_alex" + }, + "required": true, + "description": "Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "notion" + }, + "redirectUrl": { + "description": "Where the user returns after authorizing with the provider", + "type": "string", + "format": "uri" + }, + "documentLimit": { + "description": "Maximum documents this connector imports", + "type": "integer", + "minimum": 1, + "maximum": 10000 + } + }, + "required": [ + "provider" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "gmail" + }, + "redirectUrl": { + "description": "Where the user returns after authorizing with the provider", + "type": "string", + "format": "uri" + }, + "documentLimit": { + "description": "Maximum documents this connector imports", + "type": "integer", + "minimum": 1, + "maximum": 10000 + } + }, + "required": [ + "provider" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "onedrive" + }, + "redirectUrl": { + "description": "Where the user returns after authorizing with the provider", + "type": "string", + "format": "uri" + }, + "documentLimit": { + "description": "Maximum documents this connector imports", + "type": "integer", + "minimum": 1, + "maximum": 10000 + } + }, + "required": [ + "provider" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "github" + }, + "redirectUrl": { + "description": "Where the user returns after authorizing with the provider", + "type": "string", + "format": "uri" + }, + "documentLimit": { + "description": "Maximum documents this connector imports", + "type": "integer", + "minimum": 1, + "maximum": 10000 + } + }, + "required": [ + "provider" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "google-drive" + }, + "redirectUrl": { + "description": "Where the user returns after authorizing with the provider", + "type": "string", + "format": "uri" + }, + "documentLimit": { + "description": "Maximum documents this connector imports", + "type": "integer", + "minimum": 1, + "maximum": 10000 + }, + "config": { + "type": "object", + "properties": { + "syncScope": { + "description": "selected (default) syncs only files the user picks. full syncs the whole Drive.", + "type": "string", + "enum": [ + "selected", + "full" + ] + } + }, + "additionalProperties": false + } + }, + "required": [ + "provider" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "s3" + }, + "config": { + "type": "object", + "properties": { + "bucket": { + "type": "string", + "minLength": 1 + }, + "region": { + "type": "string", + "minLength": 1 + }, + "accessKeyId": { + "type": "string", + "minLength": 1 + }, + "secretAccessKey": { + "type": "string", + "minLength": 1 + }, + "endpoint": { + "description": "For S3-compatible stores such as R2 or MinIO", + "type": "string", + "format": "uri" + }, + "prefix": { + "description": "Only sync keys under this prefix", + "type": "string" + } + }, + "required": [ + "bucket", + "region", + "accessKeyId", + "secretAccessKey" + ], + "additionalProperties": false, + "description": "S3 bucket to sync. Credentials are stored encrypted and never returned." + }, + "documentLimit": { + "description": "Maximum documents this connector imports", + "type": "integer", + "minimum": 1, + "maximum": 10000 + } + }, + "required": [ + "provider", + "config" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "web-crawler" + }, + "config": { + "type": "object", + "properties": { + "startUrl": { + "type": "string", + "format": "uri" + }, + "crawlDepth": { + "description": "How many links deep to follow. Defaults to 3.", + "type": "integer", + "minimum": 1, + "maximum": 5 + } + }, + "required": [ + "startUrl" + ], + "additionalProperties": false, + "description": "Website to crawl" + }, + "documentLimit": { + "description": "Maximum documents this connector imports", + "type": "integer", + "minimum": 1, + "maximum": 10000 + } + }, + "required": [ + "provider", + "config" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "provider": { + "type": "string", + "const": "granola" + }, + "config": { + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "apiKey" + ], + "additionalProperties": false, + "description": "Granola API key. Stored encrypted and never returned." + }, + "documentLimit": { + "description": "Maximum documents this connector imports", + "type": "integer", + "minimum": 1, + "maximum": 10000 + } + }, + "required": [ + "provider", + "config" + ], + "additionalProperties": false + } + ], + "discriminator": { + "propertyName": "provider" + } + } + } + } + }, + "description": "Connect an external source to this namespace. OAuth providers return an authUrl to send the user to; providers that authenticate with config start syncing right away.", + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Connector identifier" + }, + "authUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Send the user here to authorize the provider. Null for providers that authenticate with config only." + }, + "authUrlExpiresAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "When authUrl stops working" + } + }, + "required": [ + "id", + "authUrl", + "authUrlExpiresAt" + ] + } + } + }, + "description": "Connector created" + }, + "400": { + "description": "Invalid provider config", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "402": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Plan does not include this provider" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Namespace access denied" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal server error" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Provider is temporarily unavailable" + } + }, + "summary": "Create connector", + "tags": [ + "Connectors" + ], + "x-speakeasy-group": "connectors", + "x-speakeasy-name-override": "create" + }, + "get": { + "operationId": "getNsByNamespaceConnectors", + "parameters": [ + { + "in": "path", + "name": "namespace", + "schema": { + "type": "string", + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_:-]+$", + "example": "user_alex" + }, + "required": true, + "description": "Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories." + }, + { + "in": "query", + "name": "provider", + "schema": { + "type": "string", + "enum": [ + "notion", + "google-drive", + "onedrive", + "gmail", + "github", + "web-crawler", + "s3", + "granola" + ] + }, + "description": "Only return connectors for this provider" + }, + { + "in": "query", + "name": "page", + "schema": { + "default": 1, + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "description": "One-based page number" + }, + { + "in": "query", + "name": "limit", + "schema": { + "default": 50, + "type": "integer", + "minimum": 1, + "maximum": 100 + }, + "description": "Maximum connectors to return per page" + } + ], + "description": "List the connectors that sync into this namespace.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connectors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "provider": { + "type": "string", + "enum": [ + "notion", + "google-drive", + "onedrive", + "gmail", + "github", + "web-crawler", + "s3", + "granola" + ], + "description": "External source" + }, + "status": { + "type": "string", + "enum": [ + "pending_authorization", + "active" + ], + "description": "pending_authorization until the user finishes the provider login at authUrl; DELETE cancels it. Pending connectors are not listed." + }, + "authUrl": { + "description": "Present while pending, when the caller can create connectors in this namespace: where to send the user to authorize", + "type": "string" + }, + "authUrlExpiresAt": { + "description": "Present while pending: when authUrl stops working", + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "namespace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Namespace the connector syncs into. Null only for connectors created before namespaces were required." + }, + "account": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Account on the provider side, usually an email" + }, + "capabilities": { + "type": "object", + "properties": { + "selectable": { + "type": "boolean", + "description": "Supports choosing what to sync via the hosted picker" + }, + "webhooks": { + "type": "boolean", + "description": "Receives change notifications instead of only polling" + } + }, + "required": [ + "selectable", + "webhooks" + ] + }, + "config": { + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + { + "type": "null" + } + ], + "description": "Non-secret setup, such as the S3 bucket, crawler start URL or Notion workspace. Credentials are never returned." + }, + "selection": { + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + } + } + }, + { + "type": "null" + } + ], + "description": "What syncs, grouped by kind, in the same shape PATCH takes. Null for connectors that sync everything.", + "example": { + "labels": [ + { + "id": "INBOX", + "name": "Inbox" + } + ] + } + }, + "documentLimit": { + "type": "number" + }, + "documentCount": { + "type": "number" + }, + "lastSync": { + "anyOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "running", + "completed", + "failed" + ] + }, + "errorCode": { + "anyOf": [ + { + "type": "string", + "enum": [ + "auth_expired", + "rate_limited", + "plan_required", + "provider_unavailable", + "internal" + ] + }, + { + "type": "null" + } + ] + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Customer-safe error message" + }, + "startedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "completedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "status", + "errorCode", + "error", + "startedAt", + "completedAt" + ] + }, + { + "type": "null" + } + ], + "description": "Most recent sync run" + }, + "lastSyncedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "When content last finished syncing" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "syncs": { + "description": "Present when attach=syncs", + "type": "array", + "items": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "running", + "completed", + "failed" + ] + }, + "errorCode": { + "anyOf": [ + { + "type": "string", + "enum": [ + "auth_expired", + "rate_limited", + "plan_required", + "provider_unavailable", + "internal" + ] + }, + { + "type": "null" + } + ] + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Customer-safe error message" + }, + "startedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "completedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" + }, + "trigger": { + "type": "string", + "enum": [ + "event", + "cron", + "manual" + ] + }, + "itemsProcessed": { + "type": "number" + }, + "itemsFailed": { + "type": "number" + }, + "failures": { + "type": "array", + "items": { + "type": "object", + "properties": { + "documentId": { + "type": "string" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "errorCode": { + "type": "string" + }, + "errorMessage": { + "type": "string" + }, + "failedAt": { + "type": "string" + } + }, + "required": [ + "documentId", + "title", + "errorCode", + "errorMessage", + "failedAt" + ] + }, + "description": "Failed items, capped. itemsFailed is the true count." + } + }, + "required": [ + "status", + "errorCode", + "error", + "startedAt", + "completedAt", + "id", + "trigger", + "itemsProcessed", + "itemsFailed", + "failures" + ] + } + }, + "picker": { + "description": "Present when attach=picker. Null for connectors that sync everything.", + "anyOf": [ + { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "Send the user here to choose what syncs. Works once, in any browser." + }, + "expiresAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + } + }, + "required": [ + "url", + "expiresAt" + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "provider", + "status", + "namespace", + "account", + "capabilities", + "config", + "selection", + "documentLimit", + "documentCount", + "lastSync", + "lastSyncedAt", + "createdAt" + ] + } + }, + "pagination": { + "type": "object", + "properties": { + "currentPage": { + "type": "number" + }, + "limit": { + "default": 10, + "type": "number", + "maximum": 1100 + }, + "totalItems": { + "type": "number" + }, + "totalPages": { + "type": "number" + } + }, + "required": [ + "currentPage", + "totalItems", + "totalPages" + ], + "description": "Pagination metadata", + "example": { + "currentPage": 1, + "limit": 10, + "totalItems": 100, + "totalPages": 10 + } + } + }, + "required": [ + "connectors", + "pagination" + ] + } + } + }, + "description": "Connectors in this namespace" + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Namespace access denied" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal server error" + } + }, + "summary": "List connectors", + "tags": [ + "Connectors" + ], + "x-speakeasy-group": "connectors", + "x-speakeasy-name-override": "list" + } + }, + "/ns/{namespace}/connectors/{id}": { + "get": { + "operationId": "getNsByNamespaceConnectorsById", + "parameters": [ + { + "in": "path", + "name": "namespace", + "schema": { + "type": "string", + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_:-]+$", + "example": "user_alex" + }, + "required": true, + "description": "Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories." + }, + { + "in": "path", + "name": "id", + "schema": { + "type": "string", + "minLength": 1, + "example": "PTzGiUYei7pgzg5buzZHgA" + }, + "required": true, + "description": "Connector identifier returned when the connector was created" + }, + { + "in": "query", + "name": "attach", + "schema": { + "type": "string", + "pattern": "^(syncs|picker)(,(syncs|picker))*$" + }, + "description": "Comma-separated extras. syncs: the 10 most recent sync runs with their failed items. picker: a one-time hosted picker URL." + }, + { + "in": "query", + "name": "returnUrl", + "schema": { + "type": "string", + "format": "uri" + }, + "description": "Where the hosted picker sends the user when they finish" + } + ], + "description": "Read one connector. Add attach=syncs to include its recent sync runs and the items that failed. attach=picker needs an admin with write access, since the link changes what syncs.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "provider": { + "type": "string", + "enum": [ + "notion", + "google-drive", + "onedrive", + "gmail", + "github", + "web-crawler", + "s3", + "granola" + ], + "description": "External source" + }, + "status": { + "type": "string", + "enum": [ + "pending_authorization", + "active" + ], + "description": "pending_authorization until the user finishes the provider login at authUrl; DELETE cancels it. Pending connectors are not listed." + }, + "authUrl": { + "description": "Present while pending, when the caller can create connectors in this namespace: where to send the user to authorize", + "type": "string" + }, + "authUrlExpiresAt": { + "description": "Present while pending: when authUrl stops working", + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "namespace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Namespace the connector syncs into. Null only for connectors created before namespaces were required." + }, + "account": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Account on the provider side, usually an email" + }, + "capabilities": { + "type": "object", + "properties": { + "selectable": { + "type": "boolean", + "description": "Supports choosing what to sync via the hosted picker" + }, + "webhooks": { + "type": "boolean", + "description": "Receives change notifications instead of only polling" + } + }, + "required": [ + "selectable", + "webhooks" + ] + }, + "config": { + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + { + "type": "null" + } + ], + "description": "Non-secret setup, such as the S3 bucket, crawler start URL or Notion workspace. Credentials are never returned." + }, + "selection": { + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + } + } + }, + { + "type": "null" + } + ], + "description": "What syncs, grouped by kind, in the same shape PATCH takes. Null for connectors that sync everything.", + "example": { + "labels": [ + { + "id": "INBOX", + "name": "Inbox" + } + ] + } + }, + "documentLimit": { + "type": "number" + }, + "documentCount": { + "type": "number" + }, + "lastSync": { + "anyOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "running", + "completed", + "failed" + ] + }, + "errorCode": { + "anyOf": [ + { + "type": "string", + "enum": [ + "auth_expired", + "rate_limited", + "plan_required", + "provider_unavailable", + "internal" + ] + }, + { + "type": "null" + } + ] + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Customer-safe error message" + }, + "startedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "completedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "status", + "errorCode", + "error", + "startedAt", + "completedAt" + ] + }, + { + "type": "null" + } + ], + "description": "Most recent sync run" + }, + "lastSyncedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "When content last finished syncing" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "syncs": { + "description": "Present when attach=syncs", + "type": "array", + "items": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "running", + "completed", + "failed" + ] + }, + "errorCode": { + "anyOf": [ + { + "type": "string", + "enum": [ + "auth_expired", + "rate_limited", + "plan_required", + "provider_unavailable", + "internal" + ] + }, + { + "type": "null" + } + ] + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Customer-safe error message" + }, + "startedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "completedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" + }, + "trigger": { + "type": "string", + "enum": [ + "event", + "cron", + "manual" + ] + }, + "itemsProcessed": { + "type": "number" + }, + "itemsFailed": { + "type": "number" + }, + "failures": { + "type": "array", + "items": { + "type": "object", + "properties": { + "documentId": { + "type": "string" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "errorCode": { + "type": "string" + }, + "errorMessage": { + "type": "string" + }, + "failedAt": { + "type": "string" + } + }, + "required": [ + "documentId", + "title", + "errorCode", + "errorMessage", + "failedAt" + ] + }, + "description": "Failed items, capped. itemsFailed is the true count." + } + }, + "required": [ + "status", + "errorCode", + "error", + "startedAt", + "completedAt", + "id", + "trigger", + "itemsProcessed", + "itemsFailed", + "failures" + ] + } + }, + "picker": { + "description": "Present when attach=picker. Null for connectors that sync everything.", + "anyOf": [ + { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "Send the user here to choose what syncs. Works once, in any browser." + }, + "expiresAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + } + }, + "required": [ + "url", + "expiresAt" + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "provider", + "status", + "namespace", + "account", + "capabilities", + "config", + "selection", + "documentLimit", + "documentCount", + "lastSync", + "lastSyncedAt", + "createdAt" + ] + } + } + }, + "description": "Connector" + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Namespace access denied" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Connector not found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal server error" + } + }, + "summary": "Get connector", + "tags": [ + "Connectors" + ], + "x-speakeasy-group": "connectors", + "x-speakeasy-name-override": "get" + }, + "patch": { + "operationId": "patchNsByNamespaceConnectorsById", + "parameters": [ + { + "in": "path", + "name": "namespace", + "schema": { + "type": "string", + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_:-]+$", + "example": "user_alex" + }, + "required": true, + "description": "Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories." + }, + { + "in": "path", + "name": "id", + "schema": { + "type": "string", + "minLength": 1, + "example": "PTzGiUYei7pgzg5buzZHgA" + }, + "required": true, + "description": "Connector identifier returned when the connector was created" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "selection": { + "type": "object", + "properties": { + "repos": { + "maxItems": 500, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "description": "GitHub repository ids" + }, + "labels": { + "maxItems": 500, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "description": "Gmail label ids" + }, + "files": { + "maxItems": 500, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "description": "Google Drive file ids" + }, + "folders": { + "maxItems": 500, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "description": "Google Drive folder ids" + } + }, + "additionalProperties": false, + "description": "What to sync, as ids grouped by kind. GitHub takes repos, Gmail takes labels, Google Drive takes files and folders. Replaces the current selection." + }, + "documentLimit": { + "description": "Maximum documents this connector imports", + "type": "integer", + "minimum": 1, + "maximum": 10000 + } + }, + "additionalProperties": false + } + } + } + }, + "description": "Change what a connector syncs. A new selection replaces the old one and starts a sync.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "provider": { + "type": "string", + "enum": [ + "notion", + "google-drive", + "onedrive", + "gmail", + "github", + "web-crawler", + "s3", + "granola" + ], + "description": "External source" + }, + "status": { + "type": "string", + "enum": [ + "pending_authorization", + "active" + ], + "description": "pending_authorization until the user finishes the provider login at authUrl; DELETE cancels it. Pending connectors are not listed." + }, + "authUrl": { + "description": "Present while pending, when the caller can create connectors in this namespace: where to send the user to authorize", + "type": "string" + }, + "authUrlExpiresAt": { + "description": "Present while pending: when authUrl stops working", + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "namespace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Namespace the connector syncs into. Null only for connectors created before namespaces were required." + }, + "account": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Account on the provider side, usually an email" + }, + "capabilities": { + "type": "object", + "properties": { + "selectable": { + "type": "boolean", + "description": "Supports choosing what to sync via the hosted picker" + }, + "webhooks": { + "type": "boolean", + "description": "Receives change notifications instead of only polling" + } + }, + "required": [ + "selectable", + "webhooks" + ] + }, + "config": { + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + { + "type": "null" + } + ], + "description": "Non-secret setup, such as the S3 bucket, crawler start URL or Notion workspace. Credentials are never returned." + }, + "selection": { + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + } + } + }, + { + "type": "null" + } + ], + "description": "What syncs, grouped by kind, in the same shape PATCH takes. Null for connectors that sync everything.", + "example": { + "labels": [ + { + "id": "INBOX", + "name": "Inbox" + } + ] + } + }, + "documentLimit": { + "type": "number" + }, + "documentCount": { + "type": "number" + }, + "lastSync": { + "anyOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "running", + "completed", + "failed" + ] + }, + "errorCode": { + "anyOf": [ + { + "type": "string", + "enum": [ + "auth_expired", + "rate_limited", + "plan_required", + "provider_unavailable", + "internal" + ] + }, + { + "type": "null" + } + ] + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Customer-safe error message" + }, + "startedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "completedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "status", + "errorCode", + "error", + "startedAt", + "completedAt" + ] + }, + { + "type": "null" + } + ], + "description": "Most recent sync run" + }, + "lastSyncedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "When content last finished syncing" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "syncs": { + "description": "Present when attach=syncs", + "type": "array", + "items": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "running", + "completed", + "failed" + ] + }, + "errorCode": { + "anyOf": [ + { + "type": "string", + "enum": [ + "auth_expired", + "rate_limited", + "plan_required", + "provider_unavailable", + "internal" + ] + }, + { + "type": "null" + } + ] + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Customer-safe error message" + }, + "startedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "completedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" + }, + "trigger": { + "type": "string", + "enum": [ + "event", + "cron", + "manual" + ] + }, + "itemsProcessed": { + "type": "number" + }, + "itemsFailed": { + "type": "number" + }, + "failures": { + "type": "array", + "items": { + "type": "object", + "properties": { + "documentId": { + "type": "string" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "errorCode": { + "type": "string" + }, + "errorMessage": { + "type": "string" + }, + "failedAt": { + "type": "string" + } + }, + "required": [ + "documentId", + "title", + "errorCode", + "errorMessage", + "failedAt" + ] + }, + "description": "Failed items, capped. itemsFailed is the true count." + } + }, + "required": [ + "status", + "errorCode", + "error", + "startedAt", + "completedAt", + "id", + "trigger", + "itemsProcessed", + "itemsFailed", + "failures" + ] + } + }, + "picker": { + "description": "Present when attach=picker. Null for connectors that sync everything.", + "anyOf": [ + { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "Send the user here to choose what syncs. Works once, in any browser." + }, + "expiresAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + } + }, + "required": [ + "url", + "expiresAt" + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "provider", + "status", + "namespace", + "account", + "capabilities", + "config", + "selection", + "documentLimit", + "documentCount", + "lastSync", + "lastSyncedAt", + "createdAt" + ] + } + } + }, + "description": "Updated connector" + }, + "400": { + "description": "Connector does not support selection", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Namespace access denied" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Connector not found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal server error" + } + }, + "summary": "Update connector", + "tags": [ + "Connectors" + ], + "x-speakeasy-group": "connectors", + "x-speakeasy-name-override": "update" + }, + "delete": { + "operationId": "deleteNsByNamespaceConnectorsById", + "parameters": [ + { + "in": "path", + "name": "namespace", + "schema": { + "type": "string", + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_:-]+$", + "example": "user_alex" + }, + "required": true, + "description": "Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories." + }, + { + "in": "path", + "name": "id", + "schema": { + "type": "string", + "minLength": 1, + "example": "PTzGiUYei7pgzg5buzZHgA" + }, + "required": true, + "description": "Connector identifier returned when the connector was created" + }, + { + "in": "query", + "name": "deleteDocuments", + "schema": { + "default": "true", + "type": "string", + "enum": [ + "true", + "false" + ] + }, + "description": "Also delete documents this connector imported. Defaults to true." + } + ], + "description": "Disconnect a connector and stop its webhooks. Its imported documents are deleted too unless deleteDocuments is false.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "deleted": { + "type": "boolean", + "const": true + } + }, + "required": [ + "id", + "deleted" + ] + } + } + }, + "description": "Connector deleted" + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Namespace access denied" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Connector not found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal server error" + } + }, + "summary": "Delete connector", + "tags": [ + "Connectors" + ], + "x-speakeasy-group": "connectors", + "x-speakeasy-name-override": "delete" + } + }, + "/ns/{namespace}/connectors/{id}/sync": { + "post": { + "operationId": "postNsByNamespaceConnectorsByIdSync", + "parameters": [ + { + "in": "path", + "name": "namespace", + "schema": { + "type": "string", + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_:-]+$", + "example": "user_alex" + }, + "required": true, + "description": "Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories." + }, + { + "in": "path", + "name": "id", + "schema": { + "type": "string", + "minLength": 1, + "example": "PTzGiUYei7pgzg5buzZHgA" + }, + "required": true, + "description": "Connector identifier returned when the connector was created" + } + ], + "description": "Start a sync now. Returns 409 while a sync for this connector is already running.", + "responses": { + "202": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Connector identifier" + }, + "status": { + "type": "string", + "const": "queued" + } + }, + "required": [ + "id", + "status" + ] + } + } + }, + "description": "Sync queued" + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "402": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Plan does not include this provider" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Namespace access denied" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Connector not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "A sync is already running" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal server error" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Provider is temporarily unavailable" + } + }, + "summary": "Sync connector", + "tags": [ + "Connectors" + ], + "x-speakeasy-group": "connectors", + "x-speakeasy-name-override": "sync" + } + }, + "/namespaces": { + "get": { + "operationId": "getNamespaces", + "description": "Discover every namespace available to the caller, including its purpose, document volume, memory count, and lifecycle timestamps.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Internal identifier for the namespace" + }, + "namespace": { + "type": "string", + "description": "Namespace identifier used in API paths" + }, + "documentCount": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991, + "description": "Number of active source documents in the namespace" + }, + "memoryCount": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991, + "description": "Number of current, recallable memories in the namespace" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Human-readable purpose or scope of the namespace" + }, + "system": { + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when the namespace was created" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp of the latest namespace update" + } + }, + "required": [ + "createdAt", + "updatedAt" + ], + "description": "Namespace lifecycle timestamps" + } + }, + "required": [ + "id", + "namespace", + "documentCount", + "memoryCount", + "description", + "system" + ] + } + } + } + }, + "description": "Namespaces available to the caller" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "description": "The API key cannot access this endpoint or namespace", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal server error" + } + }, + "summary": "List namespaces", + "tags": [ + "Namespaces" + ] + } + }, + "/ns": { + "get": { + "operationId": "getNs", + "description": "Discover every namespace available to the caller, including its purpose, document volume, memory count, and lifecycle timestamps.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Internal identifier for the namespace" + }, + "namespace": { + "type": "string", + "description": "Namespace identifier used in API paths" + }, + "documentCount": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991, + "description": "Number of active source documents in the namespace" + }, + "memoryCount": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991, + "description": "Number of current, recallable memories in the namespace" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Human-readable purpose or scope of the namespace" + }, + "system": { + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when the namespace was created" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp of the latest namespace update" + } + }, + "required": [ + "createdAt", + "updatedAt" + ], + "description": "Namespace lifecycle timestamps" + } + }, + "required": [ + "id", + "namespace", + "documentCount", + "memoryCount", + "description", + "system" + ] + } + } + } + }, + "description": "Namespaces available to the caller" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "description": "The API key cannot access this endpoint or namespace", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal server error" + } + }, + "summary": "List namespaces", + "tags": [ + "Namespaces" + ] + } + }, + "/ns/{namespace}": { + "get": { + "operationId": "getNsByNamespace", + "description": "Read the supporting context that helps Supermemory understand content and form better memories inside this namespace.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "namespace": { + "type": "string", + "description": "Namespace identifier used in API paths" + }, + "supportingContext": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Background that helps Supermemory interpret documents and form better memories in this namespace" + }, + "system": { + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when the namespace was created" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp of the latest settings update" + } + }, + "required": [ + "createdAt", + "updatedAt" + ], + "description": "Namespace lifecycle timestamps" + } + }, + "required": [ + "namespace", + "supportingContext", + "system" + ] + } + } + }, + "description": "Namespace settings" + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Namespace access denied" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Namespace not found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal server error" + } + }, + "summary": "Get namespace settings", + "tags": [ + "Namespaces" + ], + "parameters": [ + { + "in": "path", + "name": "namespace", + "schema": { + "type": "string", + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_:-]+$", + "example": "user_alex" + }, + "required": true, + "description": "Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories." + } + ] + }, + "patch": { + "operationId": "patchNsByNamespace", + "parameters": [ + { + "in": "path", + "name": "namespace", + "schema": { + "type": "string", + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_:-]+$", + "example": "user_alex" + }, + "required": true, + "description": "Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "supportingContext": { + "description": "Background that guides how Supermemory interprets documents and forms memories. Max 1500 characters. Set null to clear it.", + "example": "This namespace holds Acme Corp support tickets.", + "anyOf": [ + { + "type": "string", + "maxLength": 1500 + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + } + } + }, + "description": "Shape how Supermemory understands an existing namespace by updating the background context used during ingestion and memory formation.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "namespace": { + "type": "string", + "description": "Namespace identifier used in API paths" + }, + "supportingContext": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Background that helps Supermemory interpret documents and form better memories in this namespace" + }, + "system": { + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when the namespace was created" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp of the latest settings update" + } + }, + "required": [ + "createdAt", + "updatedAt" + ], + "description": "Namespace lifecycle timestamps" + } + }, + "required": [ + "namespace", + "supportingContext", + "system" + ] + } + } + }, + "description": "Updated namespace settings" + }, + "400": { + "description": "Invalid namespace settings", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Write access required" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Namespace not found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal server error" + } + }, + "summary": "Update namespace settings", + "tags": [ + "Namespaces" + ] + }, + "delete": { + "operationId": "deleteNsByNamespace", + "description": "Retire a namespace by permanently deleting its content, or preserve that knowledge by moving everything into another namespace first. Moves are queued and complete asynchronously.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "const": true, + "description": "Confirms the namespace was deleted" + }, + "namespace": { + "type": "string", + "description": "Deleted namespace identifier" + }, + "deletedDocumentsCount": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991, + "description": "Number of documents permanently removed" + }, + "deletedMemoriesCount": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991, + "description": "Number of memories permanently removed" + } + }, + "required": [ + "success", + "namespace", + "deletedDocumentsCount", + "deletedMemoriesCount" + ] + } + } + }, + "description": "Namespace permanently deleted" + }, + "202": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "const": true, + "description": "Confirms the namespace move was accepted" + }, + "status": { + "type": "string", + "const": "queued", + "description": "The move continues asynchronously after this response" + }, + "operationId": { + "type": "string", + "description": "Identifier used to trace the asynchronous move operation" + }, + "namespace": { + "type": "string", + "description": "Source namespace being removed" + }, + "moveTo": { + "type": "string", + "description": "Destination namespace receiving the content" + } + }, + "required": [ + "success", + "status", + "operationId", + "namespace", + "moveTo" + ] + } + } + }, + "description": "Namespace move queued" + }, + "400": { + "description": "Invalid move destination", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Write access required" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Namespace not found" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Namespace is busy" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal server error" + } + }, + "summary": "Delete namespace", + "tags": [ + "Namespaces" + ], + "parameters": [ + { + "in": "path", + "name": "namespace", + "schema": { + "type": "string", + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_:-]+$", + "example": "user_alex" + }, + "required": true, + "description": "Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories." + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "default": {}, + "type": "object", + "properties": { + "moveTo": { + "description": "Destination namespace that should receive this namespace's content before deletion. Omit to permanently delete the content.", + "example": "project_archive", + "type": "string", + "maxLength": 100, + "pattern": "^[a-zA-Z0-9_:-]+$" + } + }, + "additionalProperties": false + } + } + } + } + } + }, + "/organization": { + "get": { + "operationId": "getOrganization", + "description": "Read the shared background that guides memory formation across the organization, together with the number of active namespaces.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "organizationalContext": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Shared background that helps Supermemory interpret content consistently across every namespace" + }, + "namespaceCount": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991, + "description": "Number of namespaces in the organization" + } + }, + "required": [ + "organizationalContext", + "namespaceCount" + ] + } + } + }, + "description": "Organization settings retrieved successfully" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "description": "The API key cannot access this endpoint or namespace", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal server error" + } + }, + "summary": "Get organization settings", + "tags": [ + "Organization" + ] + }, + "patch": { + "operationId": "patchOrganization", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "organizationalContext": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ], + "description": "Shared background that guides memory formation across every namespace. Set null to clear it.", + "example": "Acme Corp builds developer tools for startups." + } + }, + "required": [ + "organizationalContext" + ], + "additionalProperties": false + } + } + } + }, + "description": "Give Supermemory organization-wide background that improves how content is interpreted across every namespace. Send null to remove the existing context.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "organizationalContext": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Shared background that helps Supermemory interpret content consistently across every namespace" + }, + "namespaceCount": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991, + "description": "Number of namespaces in the organization" + } + }, + "required": [ + "organizationalContext", + "namespaceCount" + ] + } + } + }, + "description": "Organization settings updated successfully" + }, + "400": { + "description": "Invalid organization settings", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ErrorResponse" + }, + { + "$ref": "#/components/schemas/ValidationErrorResponse" + } + ] + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Admin access required" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal server error" + } + }, + "summary": "Update organization settings", + "tags": [ + "Organization" + ] + } + } + } +} diff --git a/fern/overlay.yaml b/fern/overlay.yaml new file mode 100644 index 00000000..3a5af74d --- /dev/null +++ b/fern/overlay.yaml @@ -0,0 +1,43 @@ +overlay: 1.0.0 +info: { title: Supermemory v5 Python SDK surface, version: 1.0.0 } +actions: + - target: $.components.securitySchemes.bearerAuth + update: { x-fern-bearer: { name: api_key, env: SUPERMEMORY_API_KEY } } + - target: $.paths["/namespaces"] + remove: true + # root-level + - { target: '$.paths["/ns/{namespace}/document"].post', update: { x-fern-sdk-group-name: [], x-fern-sdk-method-name: add } } + - { target: '$.paths["/ns/{namespace}/search"].post', update: { x-fern-sdk-group-name: [], x-fern-sdk-method-name: search } } + - { target: '$.paths["/ns/{namespace}/profile"].post', update: { x-fern-sdk-group-name: [], x-fern-sdk-method-name: profile } } + - { target: '$.paths["/ns/{namespace}/list/{type}"].post', update: { x-fern-sdk-group-name: [], x-fern-sdk-method-name: list } } + # documents + - { target: '$.paths["/ns/{namespace}/document/batch"].post', update: { x-fern-sdk-group-name: documents, x-fern-sdk-method-name: batch_add } } + - { target: '$.paths["/ns/{namespace}/document/{id}"].get', update: { x-fern-sdk-group-name: documents, x-fern-sdk-method-name: get } } + - { target: '$.paths["/ns/{namespace}/document/{id}"].patch', update: { x-fern-sdk-group-name: documents, x-fern-sdk-method-name: update } } + - { target: '$.paths["/ns/{namespace}/document"].delete', update: { x-fern-sdk-group-name: documents, x-fern-sdk-method-name: delete } } + - { target: '$.paths["/ns/{namespace}/document/file"].post', update: { x-fern-sdk-group-name: documents, x-fern-sdk-method-name: upload_file } } + - { target: '$.paths["/ns/{namespace}/document/file/{id}"].post', update: { x-fern-sdk-group-name: documents, x-fern-sdk-method-name: replace_with_file } } + - { target: '$.paths["/ns/{namespace}/document/file/{id}"].patch', update: { x-fern-sdk-group-name: documents, x-fern-sdk-method-name: update_file } } + # memories + - { target: '$.paths["/ns/{namespace}/memories"].delete', update: { x-fern-sdk-group-name: memories, x-fern-sdk-method-name: forget } } + - { target: '$.paths["/ns/{namespace}/memories/semantic"].delete', update: { x-fern-sdk-group-name: memories, x-fern-sdk-method-name: forget_matching } } + # profiles + - { target: '$.paths["/ns/{namespace}/profile/buckets"].get', update: { x-fern-sdk-group-name: profiles, x-fern-sdk-method-name: get_buckets } } + - { target: '$.paths["/ns/{namespace}/profile/buckets"].put', update: { x-fern-sdk-group-name: profiles, x-fern-sdk-method-name: set_buckets } } + - { target: '$.paths["/ns/{namespace}/profile/buckets"].delete', update: { x-fern-sdk-group-name: profiles, x-fern-sdk-method-name: delete_buckets } } + # connectors + - { target: '$.paths["/connectors"].get', update: { x-fern-sdk-group-name: connectors, x-fern-sdk-method-name: list_providers } } + - { target: '$.paths["/ns/{namespace}/connectors"].post', update: { x-fern-sdk-group-name: connectors, x-fern-sdk-method-name: create } } + - { target: '$.paths["/ns/{namespace}/connectors"].get', update: { x-fern-sdk-group-name: connectors, x-fern-sdk-method-name: list } } + - { target: '$.paths["/ns/{namespace}/connectors/{id}"].get', update: { x-fern-sdk-group-name: connectors, x-fern-sdk-method-name: get } } + - { target: '$.paths["/ns/{namespace}/connectors/{id}"].patch', update: { x-fern-sdk-group-name: connectors, x-fern-sdk-method-name: update } } + - { target: '$.paths["/ns/{namespace}/connectors/{id}"].delete', update: { x-fern-sdk-group-name: connectors, x-fern-sdk-method-name: delete } } + - { target: '$.paths["/ns/{namespace}/connectors/{id}/sync"].post', update: { x-fern-sdk-group-name: connectors, x-fern-sdk-method-name: sync } } + # namespaces + - { target: '$.paths["/ns"].get', update: { x-fern-sdk-group-name: namespaces, x-fern-sdk-method-name: list } } + - { target: '$.paths["/ns/{namespace}"].get', update: { x-fern-sdk-group-name: namespaces, x-fern-sdk-method-name: get } } + - { target: '$.paths["/ns/{namespace}"].patch', update: { x-fern-sdk-group-name: namespaces, x-fern-sdk-method-name: update } } + - { target: '$.paths["/ns/{namespace}"].delete', update: { x-fern-sdk-group-name: namespaces, x-fern-sdk-method-name: delete } } + # organization + - { target: '$.paths["/organization"].get', update: { x-fern-sdk-group-name: organization, x-fern-sdk-method-name: get } } + - { target: '$.paths["/organization"].patch', update: { x-fern-sdk-group-name: organization, x-fern-sdk-method-name: update } } diff --git a/fern/schemas.yaml b/fern/schemas.yaml new file mode 100644 index 00000000..4534be6b --- /dev/null +++ b/fern/schemas.yaml @@ -0,0 +1,103 @@ +# Names for the types the SDK exposes. +# +# The v5 spec inlines most schemas, so without this a connector is +# GetConnectorsResponse, ListConnectorsResponseConnectorsItem, … depending on +# where it appears. scripts/name_schemas.py turns each schema below into a named +# component and replaces every structurally identical schema in the spec with a +# $ref to it — new endpoints returning the same shape pick the name up for free. +# A location that no longer resolves prints a warning and is skipped. +# +# Location: " " or a JSON pointer ("#/...") +# where: a status code (response body), "body" (request body), "param:" +# selector: .field [] array items ? non-null anyOf branch {} map values +# (field=value) the oneOf/anyOf branch whose `field` is that literal +# Top-level request bodies are only named when listed here (e.g. unions); other +# bodies stay inline because their fields become keyword arguments. + +# --- enums +TaskType: POST /ns/{namespace}/document param:taskType +DreamingMode: POST /ns/{namespace}/document param:dreaming +FileType: POST /ns/{namespace}/document/file param:fileType +SearchMode: POST /ns/{namespace}/search param:searchMode +ListType: POST /ns/{namespace}/list/{type} param:type +ListSort: POST /ns/{namespace}/list/{type} param:sort +SortOrder: POST /ns/{namespace}/list/{type} param:order +MemoryRelation: POST /ns/{namespace}/search 200.results[].included.related.parents[].relation +ConnectorProvider: GET /connectors param:provider +ConnectorStatus: GET /ns/{namespace}/connectors/{id} 200.status +ConnectorSyncStatus: GET /ns/{namespace}/connectors/{id} 200.lastSync?.status +ConnectorErrorCode: GET /ns/{namespace}/connectors/{id} 200.lastSync?.errorCode? +ConnectorSyncTrigger: GET /ns/{namespace}/connectors/{id} 200.syncs[].trigger + +# --- filters +FilterAnd: "#/components/schemas/FilterExpression/anyOf/1" +FilterOr: "#/components/schemas/FilterExpression/anyOf/2" +FilterStringEquals: "#/components/schemas/FilterPredicate/anyOf/0" +FilterValueEquals: "#/components/schemas/FilterPredicate/anyOf/1" +FilterNumberCompare: "#/components/schemas/FilterPredicate/anyOf/2" +FilterStringContains: "#/components/schemas/FilterPredicate/anyOf/3" +FilterArrayContains: "#/components/schemas/FilterPredicate/anyOf/4" + +# --- documents +MetadataValue: POST /ns/{namespace}/document body.metadata{} +DocumentInput: POST /ns/{namespace}/document/batch body.documents[] +DocumentRef: POST /ns/{namespace}/document 200 +BatchAddResult: POST /ns/{namespace}/document/batch 200 +BatchAddItemResult: POST /ns/{namespace}/document/batch 200.results[] +DeleteDocumentsResult: DELETE /ns/{namespace}/document 200 +DeleteDocumentError: DELETE /ns/{namespace}/document 200.errors[] +DocumentAttachment: GET /ns/{namespace}/document/{id} param:attach[] +FileUploadResult: POST /ns/{namespace}/document/file 200 +Document: GET /ns/{namespace}/document/{id} 200 +DocumentSystem: GET /ns/{namespace}/document/{id} 200.system +Chunk: GET /ns/{namespace}/document/{id} 200.chunks[] +ChunkSystem: GET /ns/{namespace}/document/{id} 200.chunks[].system +Memory: GET /ns/{namespace}/document/{id} 200.memories[] +MemorySystem: GET /ns/{namespace}/document/{id} 200.memories[].system + +# --- list +DocumentListItem: POST /ns/{namespace}/list/{type} 200.documents[] +ChunkListItem: POST /ns/{namespace}/list/{type} 200.chunks[] +Pagination: POST /ns/{namespace}/list/{type} 200.pagination + +# --- search +SearchResult: POST /ns/{namespace}/search 200.results[] +SearchResultIncluded: POST /ns/{namespace}/search 200.results[].included +RelatedMemories: POST /ns/{namespace}/search 200.results[].included.related +RelatedMemory: POST /ns/{namespace}/search 200.results[].included.related.parents[] + +# --- profiles +Profile: POST /ns/{namespace}/profile 200.profile +ProfileFact: POST /ns/{namespace}/profile 200.profile.static[] +ProfileBuckets: GET /ns/{namespace}/profile/buckets 200 + +# --- memories +ForgetResult: DELETE /ns/{namespace}/memories 200 +ForgetError: DELETE /ns/{namespace}/memories 200.errors[] +ForgetMatch: DELETE /ns/{namespace}/memories 200.matches[] + +# --- connectors +Connector: GET /ns/{namespace}/connectors/{id} 200 +ConnectorList: GET /connectors 200 +ConnectorCapabilities: GET /ns/{namespace}/connectors/{id} 200.capabilities +ConnectorPicker: GET /ns/{namespace}/connectors/{id} 200.picker? +ConnectorSelectionItem: GET /ns/{namespace}/connectors/{id} 200.selection?{}[] +ConnectorLastSync: GET /ns/{namespace}/connectors/{id} 200.lastSync? +ConnectorSync: GET /ns/{namespace}/connectors/{id} 200.syncs[] +ConnectorSyncFailure: GET /ns/{namespace}/connectors/{id} 200.syncs[].failures[] +ConnectorSelection: PATCH /ns/{namespace}/connectors/{id} body.selection +ConnectorSetup: POST /ns/{namespace}/connectors body +GoogleDriveConfig: POST /ns/{namespace}/connectors body(provider=google-drive).config +S3Config: POST /ns/{namespace}/connectors body(provider=s3).config +WebCrawlerConfig: POST /ns/{namespace}/connectors body(provider=web-crawler).config +GranolaConfig: POST /ns/{namespace}/connectors body(provider=granola).config +ConnectorSetupResult: POST /ns/{namespace}/connectors 201 +ConnectorSyncStarted: POST /ns/{namespace}/connectors/{id}/sync 202 +ConnectorDeleted: DELETE /ns/{namespace}/connectors/{id} 200 + +# --- namespaces and organization +Namespace: GET /ns 200[] +NamespaceSystem: GET /ns 200[].system +NamespaceDetails: GET /ns/{namespace} 200 +NamespaceDeleted: DELETE /ns/{namespace} 200 +Organization: GET /organization 200 diff --git a/noxfile.py b/noxfile.py deleted file mode 100644 index 53bca7ff..00000000 --- a/noxfile.py +++ /dev/null @@ -1,9 +0,0 @@ -import nox - - -@nox.session(reuse_venv=True, name="test-pydantic-v1") -def test_pydantic_v1(session: nox.Session) -> None: - session.install("-r", "requirements-dev.lock") - session.install("pydantic<2") - - session.run("pytest", "--showlocals", "--ignore=tests/functional", *session.posargs) diff --git a/pyproject.toml b/pyproject.toml index 76f1c583..c1002583 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,269 +1,67 @@ [project] name = "supermemory" -version = "3.62.0" -description = "The official Python library for the supermemory API" -dynamic = ["readme"] +version = "5.0.0rc1" +description = "The official Python library for the Supermemory API" +readme = "README.md" license = "Apache-2.0" -authors = [ -{ name = "Supermemory", email = "dhravya@supermemory.com" }, -] - +authors = [{ name = "Supermemory", email = "dhravya@supermemory.com" }] +requires-python = ">= 3.10" dependencies = [ - "httpx>=0.23.0, <1", - "pydantic>=1.9.0, <3", - "typing-extensions>=4.14, <5", - "anyio>=3.5.0, <5", - "distro>=1.7.0, <2", - "sniffio", + "httpx>=0.21.2", + "pydantic>=1.9.2", + "pydantic-core>=2.18.2", + "typing_extensions>=4.0.0", ] - -requires-python = ">= 3.9" classifiers = [ "Typing :: Typed", "Intended Audience :: Developers", - "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", - "Operating System :: POSIX :: Linux", - "Operating System :: Microsoft :: Windows", "Topic :: Software Development :: Libraries :: Python Modules", - "License :: OSI Approved :: Apache Software License" + "License :: OSI Approved :: Apache Software License", ] [project.urls] -Homepage = "https://github.com/supermemoryai/python-sdk" +Homepage = "https://supermemory.ai" +Documentation = "https://docs.supermemory.ai" Repository = "https://github.com/supermemoryai/python-sdk" [project.optional-dependencies] -aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"] - -[tool.rye] -managed = true -# version pins are in requirements-dev.lock -dev-dependencies = [ - "pyright==1.1.399", - "mypy==1.17", - "respx", - "pytest", - "pytest-asyncio", - "ruff", - "time-machine", - "nox", - "dirty-equals>=0.6.0", - "importlib-metadata>=6.7.0", - "rich>=13.7.1", - "pytest-xdist>=3.6.1", -] +aiohttp = ["httpx-aiohttp>=0.1.8"] -[tool.rye.scripts] -format = { chain = [ - "format:ruff", - "format:docs", - "fix:ruff", - # run formatting again to fix any inconsistencies when imports are stripped - "format:ruff", -]} -"format:docs" = "bash -c 'python scripts/utils/ruffen-docs.py README.md $(find . -type f -name api.md)'" -"format:ruff" = "ruff format" - -"lint" = { chain = [ - "check:ruff", - "typecheck", - "check:importable", -]} -"check:ruff" = "ruff check ." -"fix:ruff" = "ruff check --fix ." - -"check:importable" = "python -c 'import supermemory'" - -typecheck = { chain = [ - "typecheck:pyright", - "typecheck:mypy" -]} -"typecheck:pyright" = "pyright" -"typecheck:verify-types" = "pyright --verifytypes supermemory --ignoreexternal" -"typecheck:mypy" = "mypy ." +[dependency-groups] +dev = ["pytest>=8", "pytest-asyncio>=0.23", "mypy>=1.13", "ruff==0.16.10"] [build-system] -requires = ["hatchling==1.26.3", "hatch-fancy-pypi-readme"] +requires = ["hatchling"] build-backend = "hatchling.build" -[tool.hatch.build] -include = [ - "src/*" -] - [tool.hatch.build.targets.wheel] packages = ["src/supermemory"] -[tool.hatch.build.targets.sdist] -# Basically everything except hidden files/directories (such as .github, .devcontainers, .python-version, etc) -include = [ - "/*.toml", - "/*.json", - "/*.lock", - "/*.md", - "/mypy.ini", - "/noxfile.py", - "bin/*", - "examples/*", - "src/*", - "tests/*", -] - -[tool.hatch.metadata.hooks.fancy-pypi-readme] -content-type = "text/markdown" - -[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] -path = "README.md" - -[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] -# replace relative links with absolute links -pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' -replacement = '[\1](https://github.com/supermemoryai/python-sdk/tree/main/\g<2>)' - [tool.pytest.ini_options] testpaths = ["tests"] -addopts = "--tb=short -n auto" -xfail_strict = true asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "session" -filterwarnings = [ - "error" -] - -[tool.pyright] -# this enables practically every flag given by pyright. -# 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.9" - -exclude = [ - "_dev", - ".venv", - ".nox", - ".git", -] - -reportImplicitOverride = true -reportOverlappingOverload = false - -reportImportCycles = false -reportPrivateUsage = false [tool.mypy] -pretty = true -show_error_codes = true - -# Exclude _files.py because mypy isn't smart enough to apply -# the correct type narrowing and as this is an internal module -# it's fine to just use Pyright. -# -# We also exclude our `tests` as mypy doesn't always infer -# types correctly and Pyright will still catch any type errors. -exclude = ["src/supermemory/_files.py", "_dev/.*.py", "tests/.*"] - -strict_equality = true -implicit_reexport = true -check_untyped_defs = true -no_implicit_optional = true - -warn_return_any = true -warn_unreachable = true -warn_unused_configs = true - -# Turn these options off as it could cause conflicts -# with the Pyright options. -warn_unused_ignores = false -warn_redundant_casts = false - -disallow_any_generics = true -disallow_untyped_defs = true -disallow_untyped_calls = true -disallow_subclassing_any = true -disallow_incomplete_defs = true -disallow_untyped_decorators = true -cache_fine_grained = true - -# By default, mypy reports an error if you assign a value to the result -# of a function call that doesn't return anything. We do this in our test -# cases: -# ``` -# result = ... -# assert result is None -# ``` -# Changing this codegen to make mypy happy would increase complexity -# and would not be worth it. -disable_error_code = "func-returns-value,overload-cannot-match" - -# https://github.com/python/mypy/issues/12162 -[[tool.mypy.overrides]] -module = "black.files.*" -ignore_errors = true +# Generated code; checked for import-level and signature consistency only. +files = ["src/supermemory", "tests", "scripts", "examples"] ignore_missing_imports = true - [tool.ruff] line-length = 120 -output-format = "grouped" -target-version = "py38" - -[tool.ruff.format] -docstring-code-format = true +target-version = "py310" +# Generated; the hand-written parts live in custom/ and are linted there. +extend-exclude = ["src/supermemory"] [tool.ruff.lint] -select = [ - # isort - "I", - # bugbear rules - "B", - # remove unused imports - "F401", - # check for missing future annotations - "FA102", - # bare except statements - "E722", - # unused arguments - "ARG", - # print statements - "T201", - "T203", - # misuse of typing.TYPE_CHECKING - "TC004", - # import rules - "TID251", -] -ignore = [ - # mutable defaults - "B006", -] -unfixable = [ - # disable auto fix for print statements - "T201", - "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" - -[tool.ruff.lint.isort] -length-sort = true -length-sort-straight = true -combine-as-imports = true -extra-standard-library = ["typing_extensions"] -known-first-party = ["supermemory", "tests"] +select = ["E", "F", "I", "B", "UP"] [tool.ruff.lint.per-file-ignores] -"bin/**.py" = ["T201", "T203"] -"scripts/**.py" = ["T201", "T203"] -"tests/**.py" = ["T201", "T203"] -"examples/**.py" = ["T201", "T203"] +# Appended to the generated __init__.py, where __all__ is already defined. +"custom/__init__.tail.py" = ["F403", "F405"] + diff --git a/reference.md b/reference.md new file mode 100644 index 00000000..256bcf64 --- /dev/null +++ b/reference.md @@ -0,0 +1,2851 @@ +# Reference +
client.add(...) -> DocumentRef +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Turn text or a supported URL into searchable, evolving memory. Supply a new ID to create a document, or reuse an existing ID to append new information while preserving its history. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.add( + namespace="user_alex", + content="Supermemory turns unstructured content into evolving memory.", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**namespace:** `str` — The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + +
+
+ +
+
+ +**content:** `str` — The content to process. This may be plaintext or a URL to supported rich content. + +
+
+ +
+
+ +**task_type:** `typing.Optional[TaskType]` — Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + +
+
+ +
+
+ +**dreaming:** `typing.Optional[DreamingMode]` — Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + +
+
+ +
+
+ +**id:** `typing.Optional[str]` — An optional caller-defined document ID + +
+
+ +
+
+ +**supporting_context:** `typing.Optional[str]` — Context used to guide memory extraction within this namespace. Max 1500 characters. + +
+
+ +
+
+ +**metadata:** `typing.Optional[typing.Dict[str, MetadataValue]]` — Arbitrary metadata attached to the document and derived data + +
+
+ +
+
+ +**group:** `typing.Optional[typing.Dict[str, MetadataValue]]` — Metadata values that isolate related context within the namespace + +
+
+ +
+
+ +**date:** `typing.Optional[str]` — When the source content is from, in ISO 8601 format + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.search(...) -> SearchResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Recall the most relevant learned context and source passages from a namespace. Hybrid search combines memories with document chunks by default, with optional query rewriting, reranking, and supporting context attachments. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.search( + namespace="user_alex", + limit=10, + query="what are the API rate limits", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**namespace:** `str` — The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + +
+
+ +
+
+ +**query:** `str` — Natural-language question, topic, or phrase to retrieve relevant context for. Replaces the v4 `q` field. + +
+
+ +
+
+ +**limit:** `typing.Optional[int]` — Maximum number of results to return + +
+
+ +
+
+ +**search_mode:** `typing.Optional[SearchMode]` — Search surface. "hybrid" combines learned memories with source chunks, "memories" returns learned context, and "chunks" returns source passages. + +
+
+ +
+
+ +**filter:** `typing.Optional[FilterExpression]` — Type-safe metadata conditions applied before ranking results. Replaces the v4 `filters` field and unifies both legacy filter formats into one expression type. + +
+
+ +
+
+ +**attach:** `typing.Optional[SearchRequestAttach]` — Optional context to include alongside each matching result + +
+
+ +
+
+ +**threshold:** `typing.Optional[float]` — Minimum relevance score from 0 to 1. Raise it for precision (fewer, accurate results) or lower it for broader recall (more results). + +
+
+ +
+
+ +**rerank:** `typing.Optional[SearchRequestRerank]` — Post-retrieval ranking. "order" improves result ordering; "aggregate" also combines overlapping context into cleaner answers. This is helpful if you want to ensure the most relevant results are returned. + +
+
+ +
+
+ +**rewrite_query:** `typing.Optional[bool]` — Expand and clarify the query before retrieval to improve recall for conversational or underspecified prompts. This increases the latency by about 400ms. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.profile(...) -> ProfileResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Read a continuously maintained understanding of the subject represented by this namespace. Stable facts, evolving context, and selected custom buckets are returned together without requiring a search query. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.profile( + namespace="user_alex", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**namespace:** `str` — The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + +
+
+ +
+
+ +**filter:** `typing.Optional[FilterExpression]` — Type-safe metadata conditions that limit which memories contribute to the profile + +
+
+ +
+
+ +**buckets:** `typing.Optional[typing.List[str]]` — Custom buckets to return. Omit to return every effective bucket. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.list(...) -> ListResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Browse documents, source chunks, or learned memories through one predictable paginated contract. Choose the collection in the path; the other collection arrays remain empty for a stable response shape. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.list( + namespace="user_alex", + type="documents", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**namespace:** `str` — Namespace containing the resources to list. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope resources. + +
+
+ +
+
+ +**type:** `ListType` — Resource collection to list: source documents, extracted chunks, or learned memories + +
+
+ +
+
+ +**page:** `typing.Optional[ListRequestPage]` — One-based page number + +
+
+ +
+
+ +**limit:** `typing.Optional[ListRequestLimit]` — Maximum resources to return per page + +
+
+ +
+
+ +**sort:** `typing.Optional[ListSort]` — Field used to order results. Position is available only when listing chunks. + +
+
+ +
+
+ +**order:** `typing.Optional[SortOrder]` — Ascending or descending sort direction + +
+
+ +
+
+ +**filter:** `typing.Optional[FilterExpression]` — Type-safe metadata conditions applied before pagination + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Documents +
client.documents.delete(...) -> DeleteDocumentsResult +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Permanently remove documents and their derived knowledge by document ID or caller-defined ID. Each requested ID is handled independently so successful deletions are preserved when another ID fails. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.documents.delete( + namespace="user_alex", + ids=[ + "my-doc-123" + ], +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**namespace:** `str` — The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + +
+
+ +
+
+ +**ids:** `typing.List[str]` — Document identifiers to permanently delete from this namespace + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.documents.batch_add(...) -> BatchAddResult +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Build a knowledge base efficiently by ingesting up to 600 text or URL documents at once. Existing caller-defined IDs append new information using the same semantics as single-document ingestion. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.documents.batch_add( + namespace="user_alex", + documents=[ + { + "content": "Supermemory turns unstructured content into evolving memory." + } + ], +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**namespace:** `str` — The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + +
+
+ +
+
+ +**documents:** `typing.List[DocumentInput]` — Documents to ingest or append in one request + +
+
+ +
+
+ +**task_type:** `typing.Optional[TaskType]` — Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + +
+
+ +
+
+ +**dreaming:** `typing.Optional[DreamingMode]` — Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.documents.get(...) -> Document +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Retrieve a document's canonical content, metadata, and processing state by document ID or caller-defined ID. Optionally attach its source chunks, derived memories, or both in the same response. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.documents.get( + namespace="user_alex", + id="my-doc-123", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**namespace:** `str` — The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + +
+
+ +
+
+ +**id:** `str` — The public document ID + +
+
+ +
+
+ +**attach:** `typing.Optional[typing.Union[DocumentAttachment, typing.Sequence[DocumentAttachment]]]` — Child resources to include. Repeat the parameter to attach chunks, memories, or both. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.documents.update(...) -> DocumentRef +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Refresh an existing document without changing its stable ID. Supplied content replaces the canonical content and is reprocessed; omitted fields remain unchanged. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.documents.update( + namespace="user_alex", + id="my-doc-123", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**namespace:** `str` — The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + +
+
+ +
+
+ +**id:** `str` — The public document ID + +
+
+ +
+
+ +**task_type:** `typing.Optional[TaskType]` — Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + +
+
+ +
+
+ +**dreaming:** `typing.Optional[DreamingMode]` — Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + +
+
+ +
+
+ +**content:** `typing.Optional[str]` — The content to process. This may be plaintext or a URL to supported rich content. + +
+
+ +
+
+ +**supporting_context:** `typing.Optional[str]` — Context used to guide memory extraction within this namespace. Max 1500 characters. + +
+
+ +
+
+ +**metadata:** `typing.Optional[typing.Dict[str, MetadataValue]]` — Arbitrary metadata attached to the document and derived data + +
+
+ +
+
+ +**group:** `typing.Optional[typing.Dict[str, MetadataValue]]` — Metadata values that isolate related context within the namespace + +
+
+ +
+
+ +**date:** `typing.Optional[str]` — When the source content is from, in ISO 8601 format + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.documents.upload_file(...) -> FileUploadResult +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Transform an uploaded file into searchable knowledge and learned memory. The response returns as soon as ingestion is safely queued while extraction and memory formation continue asynchronously. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.documents.upload_file( + namespace="user_alex", + file="example_file", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**namespace:** `str` — The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + +
+
+ +
+
+ +**file:** `core.File` — File contents to upload + +
+
+ +
+
+ +**task_type:** `typing.Optional[TaskType]` — Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + +
+
+ +
+
+ +**dreaming:** `typing.Optional[DreamingMode]` — Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + +
+
+ +
+
+ +**file_type:** `typing.Optional[FileType]` — Explicit source type used when automatic inference is insufficient + +
+
+ +
+
+ +**mime_type:** `typing.Optional[str]` — Explicit MIME type used when upload metadata is insufficient + +
+
+ +
+
+ +**supporting_context:** `typing.Optional[str]` — Context used to guide memory extraction within this namespace. Max 1500 characters. + +
+
+ +
+
+ +**metadata:** `typing.Optional[str]` — JSON-encoded metadata to attach to the document and its derived memories + +
+
+ +
+
+ +**group:** `typing.Optional[str]` — JSON-encoded metadata used to keep related context isolated during processing and recall + +
+
+ +
+
+ +**date:** `typing.Optional[str]` — When the source content is from, in ISO 8601 format + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.documents.replace_with_file(...) -> FileUploadResult +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Replace an existing document with a new file while keeping its stable document ID. Content and caller metadata are overwritten, then the document is reprocessed asynchronously. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.documents.replace_with_file( + namespace="user_alex", + id="my-doc-123", + file="example_file", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**namespace:** `str` — The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + +
+
+ +
+
+ +**id:** `str` — The public document ID + +
+
+ +
+
+ +**file:** `core.File` — File contents to upload + +
+
+ +
+
+ +**task_type:** `typing.Optional[TaskType]` — Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + +
+
+ +
+
+ +**dreaming:** `typing.Optional[DreamingMode]` — Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + +
+
+ +
+
+ +**file_type:** `typing.Optional[FileType]` — Explicit source type used when automatic inference is insufficient + +
+
+ +
+
+ +**mime_type:** `typing.Optional[str]` — Explicit MIME type used when upload metadata is insufficient + +
+
+ +
+
+ +**supporting_context:** `typing.Optional[str]` — Context used to guide memory extraction within this namespace. Max 1500 characters. + +
+
+ +
+
+ +**metadata:** `typing.Optional[str]` — JSON-encoded metadata to attach to the document and its derived memories + +
+
+ +
+
+ +**group:** `typing.Optional[str]` — JSON-encoded metadata used to keep related context isolated during processing and recall + +
+
+ +
+
+ +**date:** `typing.Optional[str]` — When the source content is from, in ISO 8601 format + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.documents.update_file(...) -> FileUploadResult +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Refresh only the file-backed fields you provide. Supplying a file replaces the canonical content; omitted metadata and processing context remain unchanged. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.documents.update_file( + namespace="user_alex", + id="my-doc-123", + file="example_file", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**namespace:** `str` — The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + +
+
+ +
+
+ +**id:** `str` — The public document ID + +
+
+ +
+
+ +**task_type:** `typing.Optional[TaskType]` — Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + +
+
+ +
+
+ +**dreaming:** `typing.Optional[DreamingMode]` — Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + +
+
+ +
+
+ +**file_type:** `typing.Optional[FileType]` — Explicit source type used when automatic inference is insufficient + +
+
+ +
+
+ +**mime_type:** `typing.Optional[str]` — Explicit MIME type used when upload metadata is insufficient + +
+
+ +
+
+ +**file:** `typing.Optional[core.File]` — File contents to upload + +
+
+ +
+
+ +**supporting_context:** `typing.Optional[str]` — Context used to guide memory extraction within this namespace. Max 1500 characters. + +
+
+ +
+
+ +**metadata:** `typing.Optional[str]` — JSON-encoded metadata to attach to the document and its derived memories + +
+
+ +
+
+ +**group:** `typing.Optional[str]` — JSON-encoded metadata used to keep related context isolated during processing and recall + +
+
+ +
+
+ +**date:** `typing.Optional[str]` — When the source content is from, in ISO 8601 format + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Profiles +
client.profiles.get_buckets(...) -> ProfileBuckets +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Inspect the profile taxonomy active in this namespace. The response combines organization-wide buckets with namespace-owned additions as a concise name-to-description map. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.profiles.get_buckets( + namespace="user_alex", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**namespace:** `str` — The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.profiles.set_buckets(...) -> ProfileBuckets +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Teach Supermemory new ways to organize this namespace's profile, or refine how existing namespace-owned buckets are classified. Omitted buckets remain unchanged and organization-wide buckets stay protected. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.profiles.set_buckets( + namespace="user_alex", + buckets={ + "interests": "Topics the subject actively follows" + }, +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**namespace:** `str` — The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + +
+
+ +
+
+ +**buckets:** `typing.Dict[str, str]` — Namespace-owned bucket names mapped to descriptions that guide memory classification. Existing names are updated; new names are added. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.profiles.delete_buckets(...) -> ProfileBuckets +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Remove profile categories created specifically for this namespace. Unknown names are safely ignored, while organization-wide buckets remain protected. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.profiles.delete_buckets( + namespace="user_alex", + buckets=[ + "interests" + ], +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**namespace:** `str` — The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + +
+
+ +
+
+ +**buckets:** `typing.List[str]` — Namespace-owned bucket names to delete. Organization-level buckets are protected and cannot be removed here. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Memories +
client.memories.forget(...) -> ForgetResult +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Remove exact memories from normal recall while preserving their audit history. Each ID is handled independently and any missing or ineligible memory is reported without rolling back successful changes. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.memories.forget( + namespace="user_alex", + ids=[ + "mem_abc123" + ], +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**namespace:** `str` — Namespace containing the memories to forget. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + +
+
+ +
+
+ +**ids:** `typing.List[str]` — Memory identifiers to remove from normal recall + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.memories.forget_matching(...) -> ForgetResult +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Describe what should be forgotten in natural language, then preview or apply the matching set. Use dry-run results with the exact-ID endpoint when you need a reviewable, drift-free workflow. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.memories.forget_matching( + namespace="user_alex", + query="everything about the old pricing plans", + dry_run=True, +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**namespace:** `str` — Namespace containing the memories to forget. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + +
+
+ +
+
+ +**query:** `str` — Natural-language description of the memories that should be forgotten + +
+
+ +
+
+ +**dry_run:** `bool` — When true, preview matching memories without changing their recall state + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Connectors +
client.connectors.list_providers(...) -> ConnectorList +
+
+ +#### 📝 Description + +
+
+ +
+
+ +List connectors across every namespace this key can read. Use it for admin views that span users or projects. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.connectors.list_providers() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**provider:** `typing.Optional[ConnectorProvider]` — Only return connectors for this provider + +
+
+ +
+
+ +**page:** `typing.Optional[int]` — One-based page number + +
+
+ +
+
+ +**limit:** `typing.Optional[int]` — Maximum connectors to return per page + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.connectors.list(...) -> ConnectorList +
+
+ +#### 📝 Description + +
+
+ +
+
+ +List the connectors that sync into this namespace. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.connectors.list( + namespace="user_alex", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**namespace:** `str` — Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + +
+
+ +
+
+ +**provider:** `typing.Optional[ConnectorProvider]` — Only return connectors for this provider + +
+
+ +
+
+ +**page:** `typing.Optional[int]` — One-based page number + +
+
+ +
+
+ +**limit:** `typing.Optional[int]` — Maximum connectors to return per page + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.connectors.create(...) -> ConnectorSetupResult +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Connect an external source to this namespace. OAuth providers return an authUrl to send the user to; providers that authenticate with config start syncing right away. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.connectors.create( + namespace="user_alex", + request={ + "provider": "notion" + }, +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**namespace:** `str` — Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + +
+
+ +
+
+ +**request:** `ConnectorSetup` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.connectors.get(...) -> Connector +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Read one connector. Add attach=syncs to include its recent sync runs and the items that failed. attach=picker needs an admin with write access, since the link changes what syncs. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.connectors.get( + namespace="user_alex", + id="PTzGiUYei7pgzg5buzZHgA", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**namespace:** `str` — Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + +
+
+ +
+
+ +**id:** `str` — Connector identifier returned when the connector was created + +
+
+ +
+
+ +**attach:** `typing.Optional[str]` — Comma-separated extras. syncs: the 10 most recent sync runs with their failed items. picker: a one-time hosted picker URL. + +
+
+ +
+
+ +**return_url:** `typing.Optional[str]` — Where the hosted picker sends the user when they finish + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.connectors.delete(...) -> ConnectorDeleted +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Disconnect a connector and stop its webhooks. Its imported documents are deleted too unless deleteDocuments is false. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.connectors.delete( + namespace="user_alex", + id="PTzGiUYei7pgzg5buzZHgA", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**namespace:** `str` — Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + +
+
+ +
+
+ +**id:** `str` — Connector identifier returned when the connector was created + +
+
+ +
+
+ +**delete_documents:** `typing.Optional[DeleteConnectorsRequestDeleteDocuments]` — Also delete documents this connector imported. Defaults to true. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.connectors.update(...) -> Connector +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Change what a connector syncs. A new selection replaces the old one and starts a sync. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.connectors.update( + namespace="user_alex", + id="PTzGiUYei7pgzg5buzZHgA", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**namespace:** `str` — Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + +
+
+ +
+
+ +**id:** `str` — Connector identifier returned when the connector was created + +
+
+ +
+
+ +**selection:** `typing.Optional[ConnectorSelection]` — What to sync, as ids grouped by kind. GitHub takes repos, Gmail takes labels, Google Drive takes files and folders. Replaces the current selection. + +
+
+ +
+
+ +**document_limit:** `typing.Optional[int]` — Maximum documents this connector imports + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.connectors.sync(...) -> ConnectorSyncStarted +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Start a sync now. Returns 409 while a sync for this connector is already running. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.connectors.sync( + namespace="user_alex", + id="PTzGiUYei7pgzg5buzZHgA", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**namespace:** `str` — Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + +
+
+ +
+
+ +**id:** `str` — Connector identifier returned when the connector was created + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Namespaces +
client.namespaces.list() -> typing.List[Namespace] +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Discover every namespace available to the caller, including its purpose, document volume, memory count, and lifecycle timestamps. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.namespaces.list() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.namespaces.get(...) -> NamespaceDetails +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Read the supporting context that helps Supermemory understand content and form better memories inside this namespace. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.namespaces.get( + namespace="user_alex", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**namespace:** `str` — Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.namespaces.delete(...) -> NamespaceDeleted +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Retire a namespace by permanently deleting its content, or preserve that knowledge by moving everything into another namespace first. Moves are queued and complete asynchronously. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.namespaces.delete( + namespace="user_alex", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**namespace:** `str` — Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + +
+
+ +
+
+ +**move_to:** `typing.Optional[str]` — Destination namespace that should receive this namespace's content before deletion. Omit to permanently delete the content. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.namespaces.update(...) -> NamespaceDetails +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Shape how Supermemory understands an existing namespace by updating the background context used during ingestion and memory formation. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.namespaces.update( + namespace="user_alex", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**namespace:** `str` — Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + +
+
+ +
+
+ +**supporting_context:** `typing.Optional[str]` — Background that guides how Supermemory interprets documents and forms memories. Max 1500 characters. Set null to clear it. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## Organization +
client.organization.get() -> Organization +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Read the shared background that guides memory formation across the organization, together with the number of active namespaces. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.organization.get() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.organization.update(...) -> Organization +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Give Supermemory organization-wide background that improves how content is interpreted across every namespace. Send null to remove the existing context. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from supermemory import Supermemory +from supermemory.environment import SupermemoryEnvironment + +client = Supermemory( + api_key="", + environment=SupermemoryEnvironment.DEFAULT, +) + +client.organization.update( + organizational_context=None, +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**organizational_context:** `typing.Optional[str]` — Shared background that guides memory formation across every namespace. Set null to clear it. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ diff --git a/release-please-config.json b/release-please-config.json deleted file mode 100644 index 247d1fda..00000000 --- a/release-please-config.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "packages": { - ".": {} - }, - "$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json", - "include-v-in-tag": true, - "include-component-in-tag": false, - "versioning": "prerelease", - "prerelease": true, - "bump-minor-pre-major": true, - "bump-patch-for-minor-pre-major": false, - "pull-request-header": "Automated Release PR", - "pull-request-title-pattern": "release: ${version}", - "changelog-sections": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "perf", - "section": "Performance Improvements" - }, - { - "type": "revert", - "section": "Reverts" - }, - { - "type": "chore", - "section": "Chores" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "style", - "section": "Styles" - }, - { - "type": "refactor", - "section": "Refactors" - }, - { - "type": "test", - "section": "Tests", - "hidden": true - }, - { - "type": "build", - "section": "Build System" - }, - { - "type": "ci", - "section": "Continuous Integration", - "hidden": true - } - ], - "release-type": "python", - "extra-files": [ - "src/supermemory/_version.py" - ] -} \ No newline at end of file diff --git a/requirements-dev.lock b/requirements-dev.lock deleted file mode 100644 index 2777273c..00000000 --- a/requirements-dev.lock +++ /dev/null @@ -1,149 +0,0 @@ -# generated by rye -# use `rye lock` or `rye sync` to update this lockfile -# -# last locked with the following flags: -# pre: false -# features: [] -# all-features: true -# with-sources: false -# generate-hashes: false -# universal: false - --e file:. -aiohappyeyeballs==2.6.1 - # via aiohttp -aiohttp==3.13.3 - # via httpx-aiohttp - # via supermemory -aiosignal==1.4.0 - # via aiohttp -annotated-types==0.7.0 - # via pydantic -anyio==4.12.1 - # via httpx - # via supermemory -argcomplete==3.6.3 - # via nox -async-timeout==5.0.1 - # via aiohttp -attrs==25.4.0 - # via aiohttp - # via nox -backports-asyncio-runner==1.2.0 - # via pytest-asyncio -certifi==2026.1.4 - # via httpcore - # via httpx -colorlog==6.10.1 - # via nox -dependency-groups==1.3.1 - # via nox -dirty-equals==0.11 -distlib==0.4.0 - # via virtualenv -distro==1.9.0 - # via supermemory -exceptiongroup==1.3.1 - # via anyio - # via pytest -execnet==2.1.2 - # via pytest-xdist -filelock==3.19.1 - # via virtualenv -frozenlist==1.8.0 - # via aiohttp - # via aiosignal -h11==0.16.0 - # via httpcore -httpcore==1.0.9 - # via httpx -httpx==0.28.1 - # via httpx-aiohttp - # via respx - # via supermemory -httpx-aiohttp==0.1.12 - # via supermemory -humanize==4.13.0 - # via nox -idna==3.11 - # via anyio - # via httpx - # via yarl -importlib-metadata==8.7.1 -iniconfig==2.1.0 - # via pytest -markdown-it-py==3.0.0 - # via rich -mdurl==0.1.2 - # via markdown-it-py -multidict==6.7.0 - # via aiohttp - # via yarl -mypy==1.17.0 -mypy-extensions==1.1.0 - # via mypy -nodeenv==1.10.0 - # via pyright -nox==2025.11.12 -packaging==25.0 - # via dependency-groups - # via nox - # via pytest -pathspec==1.0.3 - # via mypy -platformdirs==4.4.0 - # via virtualenv -pluggy==1.6.0 - # via pytest -propcache==0.4.1 - # via aiohttp - # via yarl -pydantic==2.12.5 - # via supermemory -pydantic-core==2.41.5 - # via pydantic -pygments==2.19.2 - # via pytest - # via rich -pyright==1.1.399 -pytest==8.4.2 - # via pytest-asyncio - # via pytest-xdist -pytest-asyncio==1.2.0 -pytest-xdist==3.8.0 -python-dateutil==2.9.0.post0 - # via time-machine -respx==0.22.0 -rich==14.2.0 -ruff==0.14.13 -six==1.17.0 - # via python-dateutil -sniffio==1.3.1 - # via supermemory -time-machine==2.19.0 -tomli==2.4.0 - # via dependency-groups - # via mypy - # via nox - # via pytest -typing-extensions==4.15.0 - # via aiosignal - # via anyio - # via exceptiongroup - # via multidict - # via mypy - # via pydantic - # via pydantic-core - # via pyright - # via pytest-asyncio - # via supermemory - # via typing-inspection - # via virtualenv -typing-inspection==0.4.2 - # via pydantic -virtualenv==20.36.1 - # via nox -yarl==1.22.0 - # via aiohttp -zipp==3.23.0 - # via importlib-metadata diff --git a/requirements.lock b/requirements.lock deleted file mode 100644 index 689a8533..00000000 --- a/requirements.lock +++ /dev/null @@ -1,76 +0,0 @@ -# generated by rye -# use `rye lock` or `rye sync` to update this lockfile -# -# last locked with the following flags: -# pre: false -# features: [] -# all-features: true -# with-sources: false -# generate-hashes: false -# universal: false - --e file:. -aiohappyeyeballs==2.6.1 - # via aiohttp -aiohttp==3.13.3 - # via httpx-aiohttp - # via supermemory -aiosignal==1.4.0 - # via aiohttp -annotated-types==0.7.0 - # via pydantic -anyio==4.12.1 - # via httpx - # via supermemory -async-timeout==5.0.1 - # via aiohttp -attrs==25.4.0 - # via aiohttp -certifi==2026.1.4 - # via httpcore - # via httpx -distro==1.9.0 - # via supermemory -exceptiongroup==1.3.1 - # via anyio -frozenlist==1.8.0 - # via aiohttp - # via aiosignal -h11==0.16.0 - # via httpcore -httpcore==1.0.9 - # via httpx -httpx==0.28.1 - # via httpx-aiohttp - # via supermemory -httpx-aiohttp==0.1.12 - # via supermemory -idna==3.11 - # via anyio - # via httpx - # via yarl -multidict==6.7.0 - # via aiohttp - # via yarl -propcache==0.4.1 - # via aiohttp - # via yarl -pydantic==2.12.5 - # via supermemory -pydantic-core==2.41.5 - # via pydantic -sniffio==1.3.1 - # via supermemory -typing-extensions==4.15.0 - # via aiosignal - # via anyio - # via exceptiongroup - # via multidict - # via pydantic - # via pydantic-core - # via supermemory - # via typing-inspection -typing-inspection==0.4.2 - # via pydantic -yarl==1.22.0 - # via aiohttp diff --git a/scripts/apply_custom.py b/scripts/apply_custom.py new file mode 100644 index 00000000..833164a6 --- /dev/null +++ b/scripts/apply_custom.py @@ -0,0 +1,103 @@ +"""Layer the hand-written code in custom/ onto the freshly generated package. + +- copies custom/supermemory/* into src/supermemory/ +- applies custom/patches/*.patch (small additions to generated core files) +- adds the 3.x per-call options (extra_headers, extra_query, extra_body, + timeout) to every generated method signature, next to request_options, so + type checkers accept them; the client wrapper in _compat.py translates them +- removes every name that custom/supermemory/_compat.py exports from the + generated __init__.py lazy-import tables, then appends custom/__init__.tail.py, + so `supermemory.Supermemory`, `supermemory.NotFoundError`, … resolve to the + compatible versions for both the runtime and type checkers. +""" + +import ast +import re +import shutil +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +PACKAGE = ROOT / "src" / "supermemory" +CUSTOM = ROOT / "custom" + + +def compat_exports() -> set[str]: + tree = ast.parse((CUSTOM / "supermemory" / "_compat.py").read_text()) + for node in tree.body: + if isinstance(node, ast.Assign) and any(isinstance(t, ast.Name) and t.id == "__all__" for t in node.targets): + return set(ast.literal_eval(node.value)) + raise SystemExit("custom/supermemory/_compat.py has no __all__") + + +PER_CALL_OPTIONS = ( + ", extra_headers: typing.Optional[typing.Mapping[str, str]] = None" + ", extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None" + ", extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None" + ", timeout: typing.Union[float, httpx.Timeout, None] = None" +) +PER_CALL_NAMES = {"extra_headers", "extra_query", "extra_body", "timeout"} + + +def add_per_call_options(path: Path) -> None: + source = path.read_text() + lines = source.splitlines(keepends=True) + inserts: list[tuple[int, int]] = [] + for node in ast.walk(ast.parse(source)): + if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + continue + names = [a.arg for a in node.args.kwonlyargs] + if "request_options" not in names: + continue + clash = PER_CALL_NAMES.intersection(names) + if clash: + raise SystemExit(f"{path}:{node.lineno} {node.name}() already has {clash}; rename it in fern/overlay.yaml") + default = node.args.kw_defaults[names.index("request_options")] + assert default is not None and default.end_lineno is not None and default.end_col_offset is not None + inserts.append((default.end_lineno - 1, default.end_col_offset)) + if not inserts: + return + for line, col in sorted(inserts, reverse=True): + text = lines[line] + # col is a UTF-8 byte offset + raw = text.encode() + lines[line] = (raw[:col] + PER_CALL_OPTIONS.encode() + raw[col:]).decode() + source = "".join(lines) + if not re.search(r"^import httpx$", source, re.M): + source = re.sub(r"^import typing$", "import typing\n\nimport httpx", source, count=1, flags=re.M) + path.write_text(source) + + +def main() -> None: + shutil.copytree(CUSTOM / "supermemory", PACKAGE, dirs_exist_ok=True) + for patch in sorted((CUSTOM / "patches").glob("*.patch")): + subprocess.run(["patch", "-p1", "--forward", "--no-backup-if-mismatch", "-i", str(patch)], cwd=ROOT, check=True) + for path in sorted(PACKAGE.rglob("*client.py")): + if "core" not in path.relative_to(PACKAGE).parts: + add_per_call_options(path) + + names = compat_exports() + init = PACKAGE / "__init__.py" + lines = [] + for line in init.read_text().splitlines(): + stripped = line.strip() + # ` NotFoundError,` inside a TYPE_CHECKING `from .x import (...)` block + if stripped.rstrip(",") in names and stripped.endswith(","): + continue + # `"NotFoundError": ".errors",` in the lazy-import mapping + match = re.fullmatch(r'"(\w+)": "\.[\w.]+",', stripped) + if match and match.group(1) in names: + continue + # `from .client import AsyncSupermemory, Supermemory` + match = re.fullmatch(r"(\s*from \.[\w.]+ import )([\w, ]+)", line) + if match: + kept = [n.strip() for n in match.group(2).split(",") if n.strip() not in names] + if not kept: + continue + line = match.group(1) + ", ".join(kept) + lines.append(line) + init.write_text("\n".join(lines) + "\n" + (CUSTOM / "__init__.tail.py").read_text()) + + +if __name__ == "__main__": + main() diff --git a/scripts/bootstrap b/scripts/bootstrap deleted file mode 100755 index fe8451e4..00000000 --- a/scripts/bootstrap +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "${SKIP_BREW:-}" != "1" ] && [ -t 0 ]; then - brew bundle check >/dev/null 2>&1 || { - echo -n "==> Install Homebrew dependencies? (y/N): " - read -r response - case "$response" in - [yY][eE][sS]|[yY]) - brew bundle - ;; - *) - ;; - esac - echo - } -fi - -echo "==> Installing Python dependencies…" - -# experimental uv support makes installations significantly faster -rye config --set-bool behavior.use-uv=true - -rye sync --all-features diff --git a/scripts/bump_version.py b/scripts/bump_version.py new file mode 100644 index 00000000..eb61cc69 --- /dev/null +++ b/scripts/bump_version.py @@ -0,0 +1,30 @@ +"""Bump the version in pyproject.toml: 5.0.0rc1 -> 5.0.0rc2, 5.0.0 -> 5.0.1. + +Used by the Generate SDK workflow when a spec change alters the generated code. +""" + +import re +from pathlib import Path + +PYPROJECT = Path(__file__).resolve().parent.parent / "pyproject.toml" + + +def bump(version: str) -> str: + pre = re.fullmatch(r"(.+?)(a|b|rc)(\d+)", version) + if pre: + return f"{pre.group(1)}{pre.group(2)}{int(pre.group(3)) + 1}" + major, minor, patch = (int(part) for part in version.split(".")) + return f"{major}.{minor}.{patch + 1}" + + +def main() -> None: + text = PYPROJECT.read_text() + current = re.search(r'^version = "(.+)"$', text, re.M) + assert current, "no version in pyproject.toml" + new = bump(current.group(1)) + PYPROJECT.write_text(text.replace(current.group(0), f'version = "{new}"', 1)) + print(new) + + +if __name__ == "__main__": + main() diff --git a/scripts/format b/scripts/format deleted file mode 100755 index 667ec2d7..00000000 --- a/scripts/format +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -echo "==> Running formatters" -rye run format diff --git a/scripts/generate b/scripts/generate new file mode 100755 index 00000000..7ddd571f --- /dev/null +++ b/scripts/generate @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Regenerate src/supermemory from the live v5 OpenAPI spec. +# +# scripts/generate # fetch the latest spec, then generate +# SKIP_FETCH=1 scripts/generate # regenerate from the committed fern/openapi.json +# +# Requires Docker (Fern runs its Python generator as a container), bun, and uv. +set -euo pipefail +cd "$(dirname "$0")/.." + +SPEC_URL="${SUPERMEMORY_OPENAPI_URL:-https://api.supermemory.ai/v5/openapi}" +FERN_VERSION=$(python3 -c 'import json; print(json.load(open("fern/fern.config.json"))["version"])') +SDK_VERSION=$(python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])') + +docker info >/dev/null 2>&1 || { echo "error: a running Docker daemon is required" >&2; exit 1; } + +if [ -z "${SKIP_FETCH:-}" ]; then + echo "==> Fetching $SPEC_URL" + curl -fsSL "$SPEC_URL" | python3 -c 'import json,sys; json.dump(json.load(sys.stdin), sys.stdout, indent=2); print()' > fern/openapi.json +fi + +# Name the spec's inline schemas (fern/schemas.yaml) in a copy Fern reads. +python3 scripts/name_schemas.py fern/openapi.json fern/openapi.sdk.json + +echo "==> Generating supermemory $SDK_VERSION with fern-api@$FERN_VERSION" +FERN_NO_VERSION_REDIRECTION=true bunx "fern-api@$FERN_VERSION" generate \ + --local --group python-sdk --version "$SDK_VERSION" --force 2>&1 | tee .fern-generate.log +# fern exits 0 on some generator failures; treat any reported error as fatal. +if grep -qE "Unexpected error|Failed|CliError" .fern-generate.log || [ ! -f src/supermemory/client.py ]; then + rm -f .fern-generate.log + echo "error: Fern generation failed" >&2 + exit 1 +fi +rm -f .fern-generate.log + +# Fern writes repo-level docs, its own tests, and run metadata (which embeds the +# git commit, so it would never diff clean) inside the package directory. +mv src/supermemory/reference.md reference.md +rm -rf src/supermemory/README.md src/supermemory/CONTRIBUTING.md src/supermemory/tests src/supermemory/.fern +touch src/supermemory/py.typed + +# Hand-written code (custom/) on top of the generated package. +python3 scripts/apply_custom.py + +# Re-format after the edits above (pinned, so output is reproducible). +uvx --quiet "ruff==0.16.10" format --quiet --isolated --line-length 120 src/supermemory + +echo "==> Done. Review with: git diff --stat" diff --git a/scripts/lint b/scripts/lint deleted file mode 100755 index 9d31eaa4..00000000 --- a/scripts/lint +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -if [ "$1" = "--fix" ]; then - echo "==> Running lints with --fix" - rye run fix:ruff -else - echo "==> Running lints" - rye run lint -fi - -echo "==> Making sure it imports" -rye run python -c 'import supermemory' diff --git a/scripts/name_schemas.py b/scripts/name_schemas.py new file mode 100644 index 00000000..2c9cab12 --- /dev/null +++ b/scripts/name_schemas.py @@ -0,0 +1,176 @@ +"""Give the inline schemas in fern/openapi.json the names in fern/schemas.yaml. + +Each named schema becomes components/schemas/, and every structurally +identical schema elsewhere (ignoring descriptions and examples) becomes a $ref +to it. Locations that no longer resolve are skipped with a warning, so a spec +change never blocks generation; it only falls back to Fern's default names. + + python3 scripts/name_schemas.py fern/openapi.json fern/openapi.sdk.json +""" + +from __future__ import annotations + +import json +import os +import re +import sys +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parent.parent +NAMES = ROOT / "fern" / "schemas.yaml" +IGNORED_KEYS = {"description", "example", "examples", "title"} +# Request bodies stay inline: Fern turns their fields into keyword arguments. +REQUEST_BODY = re.compile(r"^/paths/[^/]+/[a-z]+/requestBody/content/[^/]+/schema$") + +Json = Any + + +def warn(message: str) -> None: + prefix = "::warning::" if "GITHUB_ACTIONS" in os.environ else "warning: " + print(f"{prefix}schemas.yaml: {message}", file=sys.stderr) + + +def load_names() -> list[tuple[str, str]]: + names = [] + for line in NAMES.read_text().splitlines(): + line = line.split(" #", 1)[0].strip() + if not line or line.startswith("#"): + continue + name, _, location = line.partition(":") + names.append((name.strip(), location.strip().strip('"'))) + return names + + +def escape(key: str) -> str: + return key.replace("~", "~0").replace("/", "~1") + + +def get(spec: Json, pointer: str) -> Json: + node = spec + for part in pointer.lstrip("/").split("/"): + part = part.replace("~1", "/").replace("~0", "~") + node = node[int(part)] if isinstance(node, list) else node[part] + return node + + +def resolve(spec: Json, location: str) -> str: + """Turn a schemas.yaml location into a JSON pointer, checking it exists.""" + if location.startswith("#"): + pointer = location[1:] + get(spec, pointer) + return pointer + method, path, rest = location.split(" ", 2) + match = re.match(r"(param:\w+|body|\d{3})(.*)", rest) + if not match: + raise KeyError(rest) + where, selector = match.groups() + op = f"/paths/{escape(path)}/{method.lower()}" + get(spec, op) + if where == "body": + content = get(spec, f"{op}/requestBody/content") + pointer = f"{op}/requestBody/content/{escape(next(iter(content)))}/schema" + elif where.startswith("param:"): + params = get(spec, f"{op}/parameters") + index = next(i for i, p in enumerate(params) if p["name"] == where[6:]) + pointer = f"{op}/parameters/{index}/schema" + else: + pointer = f"{op}/responses/{where}/content/application~1json/schema" + for token in re.findall(r"\.\w+|\[\]|\?|\{\}|\(\w+=[\w-]+\)", selector): + node = get(spec, pointer) + if token.startswith("("): + field, value = token[1:-1].split("=") + key = "oneOf" if "oneOf" in node else "anyOf" + index = next(i for i, b in enumerate(node[key]) if _literal(b, field) == value) + pointer += f"/{key}/{index}" + elif token == "[]": + pointer += "/items" + elif token == "{}": + pointer += "/additionalProperties" + elif token == "?": + key = "anyOf" if "anyOf" in node else "oneOf" + index = next(i for i, b in enumerate(node[key]) if b.get("type") != "null") + pointer += f"/{key}/{index}" + else: + pointer += f"/properties/{token[1:]}" + get(spec, pointer) + return pointer + + +def _literal(schema: Json, field: str) -> Json: + prop = schema.get("properties", {}).get(field, {}) + return prop.get("const", (prop.get("enum") or [None])[0]) + + +def canonical(node: Json) -> str: + def strip(value: Json) -> Json: + if isinstance(value, dict): + return {k: strip(v) for k, v in value.items() if k not in IGNORED_KEYS} + if isinstance(value, list): + return [strip(v) for v in value] + return value + + return json.dumps(strip(node), sort_keys=True) + + +def replace_all(spec: Json, key: str, ref: str, skip: str, source: str) -> int: + """Replace every schema whose canonical form is `key` with a $ref.""" + count = 0 + + def visit(node: Json, pointer: str) -> Json: + nonlocal count + if isinstance(node, dict): + inline_body = REQUEST_BODY.match(pointer) and pointer != source + if pointer != skip and not inline_body and canonical(node) == key: + count += 1 + replacement: dict[str, Json] = {"$ref": ref} + if "description" in node: + replacement["description"] = node["description"] + return replacement + return {k: visit(v, f"{pointer}/{escape(k)}") for k, v in node.items()} + if isinstance(node, list): + return [visit(v, f"{pointer}/{i}") for i, v in enumerate(node)] + return node + + for top in ("paths", "components"): + spec[top] = visit(spec[top], f"/{top}") + return count + + +def main(source: str, target: str) -> None: + spec = json.loads(Path(source).read_text()) + existing = set(spec.setdefault("components", {}).setdefault("schemas", {})) + resolved: list[tuple[str, str]] = [] + for name, location in load_names(): + if name in existing: + warn(f"{name} already exists in the spec; skipped") + continue + try: + resolved.append((name, resolve(spec, location))) + except (KeyError, IndexError, StopIteration, ValueError): + warn(f"{name}: {location} not found; skipped") + # Innermost first, so outer schemas compare equal after their parts are named. + resolved.sort(key=lambda item: -item[1].count("/")) + names_by_pointer = dict((pointer, name) for name, pointer in resolved) + for pointer, name in names_by_pointer.items(): + try: + node = get(spec, pointer) + except (KeyError, IndexError): + warn(f"{name}: moved by an earlier name; skipped") + continue + if "$ref" in node: + warn(f"{name}: already named {node['$ref'].rsplit('/', 1)[-1]}; skipped") + continue + spec["components"]["schemas"][name] = node # replace_all rebuilds "components" + replace_all( + spec, + canonical(node), + f"#/components/schemas/{name}", + skip=f"/components/schemas/{escape(name)}", + source=pointer, + ) + Path(target).write_text(json.dumps(spec, indent=2) + "\n") + + +if __name__ == "__main__": + main(sys.argv[1], sys.argv[2]) diff --git a/scripts/test b/scripts/test deleted file mode 100755 index 39729d09..00000000 --- a/scripts/test +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - - - -export DEFER_PYDANTIC_BUILD=false - -echo "==> Running tests" -rye run pytest "$@" - -echo "==> Running Pydantic v1 tests" -rye run nox -s test-pydantic-v1 -- "$@" diff --git a/scripts/utils/ruffen-docs.py b/scripts/utils/ruffen-docs.py deleted file mode 100644 index 0cf2bd2f..00000000 --- a/scripts/utils/ruffen-docs.py +++ /dev/null @@ -1,167 +0,0 @@ -# fork of https://github.com/asottile/blacken-docs adapted for ruff -from __future__ import annotations - -import re -import sys -import argparse -import textwrap -import contextlib -import subprocess -from typing import Match, Optional, Sequence, Generator, NamedTuple, cast - -MD_RE = re.compile( - r"(?P^(?P *)```\s*python\n)" r"(?P.*?)" r"(?P^(?P=indent)```\s*$)", - re.DOTALL | re.MULTILINE, -) -MD_PYCON_RE = re.compile( - r"(?P^(?P *)```\s*pycon\n)" r"(?P.*?)" r"(?P^(?P=indent)```.*$)", - re.DOTALL | re.MULTILINE, -) -PYCON_PREFIX = ">>> " -PYCON_CONTINUATION_PREFIX = "..." -PYCON_CONTINUATION_RE = re.compile( - rf"^{re.escape(PYCON_CONTINUATION_PREFIX)}( |$)", -) -DEFAULT_LINE_LENGTH = 100 - - -class CodeBlockError(NamedTuple): - offset: int - exc: Exception - - -def format_str( - src: str, -) -> tuple[str, Sequence[CodeBlockError]]: - errors: list[CodeBlockError] = [] - - @contextlib.contextmanager - def _collect_error(match: Match[str]) -> Generator[None, None, None]: - try: - yield - except Exception as e: - errors.append(CodeBlockError(match.start(), e)) - - def _md_match(match: Match[str]) -> str: - code = textwrap.dedent(match["code"]) - with _collect_error(match): - code = format_code_block(code) - code = textwrap.indent(code, match["indent"]) - return f"{match['before']}{code}{match['after']}" - - def _pycon_match(match: Match[str]) -> str: - code = "" - fragment = cast(Optional[str], None) - - def finish_fragment() -> None: - nonlocal code - nonlocal fragment - - if fragment is not None: - with _collect_error(match): - fragment = format_code_block(fragment) - fragment_lines = fragment.splitlines() - code += f"{PYCON_PREFIX}{fragment_lines[0]}\n" - for line in fragment_lines[1:]: - # Skip blank lines to handle Black adding a blank above - # functions within blocks. A blank line would end the REPL - # continuation prompt. - # - # >>> if True: - # ... def f(): - # ... pass - # ... - if line: - code += f"{PYCON_CONTINUATION_PREFIX} {line}\n" - if fragment_lines[-1].startswith(" "): - code += f"{PYCON_CONTINUATION_PREFIX}\n" - fragment = None - - indentation = None - for line in match["code"].splitlines(): - orig_line, line = line, line.lstrip() - if indentation is None and line: - indentation = len(orig_line) - len(line) - continuation_match = PYCON_CONTINUATION_RE.match(line) - if continuation_match and fragment is not None: - fragment += line[continuation_match.end() :] + "\n" - else: - finish_fragment() - if line.startswith(PYCON_PREFIX): - fragment = line[len(PYCON_PREFIX) :] + "\n" - else: - code += orig_line[indentation:] + "\n" - finish_fragment() - return code - - def _md_pycon_match(match: Match[str]) -> str: - code = _pycon_match(match) - code = textwrap.indent(code, match["indent"]) - return f"{match['before']}{code}{match['after']}" - - src = MD_RE.sub(_md_match, src) - src = MD_PYCON_RE.sub(_md_pycon_match, src) - return src, errors - - -def format_code_block(code: str) -> str: - return subprocess.check_output( - [ - sys.executable, - "-m", - "ruff", - "format", - "--stdin-filename=script.py", - f"--line-length={DEFAULT_LINE_LENGTH}", - ], - encoding="utf-8", - input=code, - ) - - -def format_file( - filename: str, - skip_errors: bool, -) -> int: - with open(filename, encoding="UTF-8") as f: - contents = f.read() - new_contents, errors = format_str(contents) - for error in errors: - lineno = contents[: error.offset].count("\n") + 1 - print(f"{filename}:{lineno}: code block parse error {error.exc}") - if errors and not skip_errors: - return 1 - if contents != new_contents: - print(f"{filename}: Rewriting...") - with open(filename, "w", encoding="UTF-8") as f: - f.write(new_contents) - return 0 - else: - return 0 - - -def main(argv: Sequence[str] | None = None) -> int: - parser = argparse.ArgumentParser() - parser.add_argument( - "-l", - "--line-length", - type=int, - default=DEFAULT_LINE_LENGTH, - ) - parser.add_argument( - "-S", - "--skip-string-normalization", - action="store_true", - ) - parser.add_argument("-E", "--skip-errors", action="store_true") - parser.add_argument("filenames", nargs="*") - args = parser.parse_args(argv) - - retv = 0 - for filename in args.filenames: - retv |= format_file(filename, skip_errors=args.skip_errors) - return retv - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh deleted file mode 100755 index 203177fc..00000000 --- a/scripts/utils/upload-artifact.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash -set -exuo pipefail - -FILENAME=$(basename dist/*.whl) - -RESPONSE=$(curl -X POST "$URL?filename=$FILENAME" \ - -H "Authorization: Bearer $AUTH" \ - -H "Content-Type: application/json") - -SIGNED_URL=$(echo "$RESPONSE" | jq -r '.url') - -if [[ "$SIGNED_URL" == "null" ]]; then - echo -e "\033[31mFailed to get signed URL.\033[0m" - exit 1 -fi - -UPLOAD_RESPONSE=$(curl -v -X PUT \ - -H "Content-Type: binary/octet-stream" \ - --data-binary "@dist/$FILENAME" "$SIGNED_URL" 2>&1) - -if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then - echo -e "\033[32mUploaded build to Stainless storage.\033[0m" - echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/supermemory-new-python/$SHA/$FILENAME'\033[0m" -else - echo -e "\033[31mFailed to upload artifact.\033[0m" - exit 1 -fi diff --git a/src/supermemory/__init__.py b/src/supermemory/__init__.py index 78f13680..7c2b2cb5 100644 --- a/src/supermemory/__init__.py +++ b/src/supermemory/__init__.py @@ -1,102 +1,700 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +# This file was auto-generated by Fern from our API Definition. -import typing as _t +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import ( + BadRequestErrorBody, + BatchAddItemResult, + BatchAddResult, + Chunk, + ChunkListItem, + ChunkSystem, + Connector, + ConnectorCapabilities, + ConnectorConfigValue, + ConnectorDeleted, + ConnectorErrorCode, + ConnectorLastSync, + ConnectorList, + ConnectorPicker, + ConnectorProvider, + ConnectorSelection, + ConnectorSelectionItem, + ConnectorSetup, + ConnectorSetupGithub, + ConnectorSetupGmail, + ConnectorSetupGoogleDrive, + ConnectorSetupGranola, + ConnectorSetupNotion, + ConnectorSetupOnedrive, + ConnectorSetupResult, + ConnectorSetupS3, + ConnectorSetupWebCrawler, + ConnectorSetup_Github, + ConnectorSetup_Gmail, + ConnectorSetup_GoogleDrive, + ConnectorSetup_Granola, + ConnectorSetup_Notion, + ConnectorSetup_Onedrive, + ConnectorSetup_S3, + ConnectorSetup_WebCrawler, + ConnectorStatus, + ConnectorSync, + ConnectorSyncFailure, + ConnectorSyncStarted, + ConnectorSyncStatus, + ConnectorSyncTrigger, + DeleteDocumentError, + DeleteDocumentsResult, + Document, + DocumentAttachment, + DocumentInput, + DocumentListItem, + DocumentRef, + DocumentSystem, + DreamingMode, + ErrorResponse, + FileType, + FileUploadResult, + FilterAnd, + FilterArrayContains, + FilterArrayContainsOperator, + FilterExpression, + FilterNumberCompare, + FilterNumberCompareOperator, + FilterOr, + FilterPredicate, + FilterStringContains, + FilterStringContainsOperator, + FilterStringEquals, + FilterStringEqualsOperator, + FilterValueEquals, + FilterValueEqualsOperator, + FilterValueEqualsValue, + ForgetError, + ForgetMatch, + ForgetResult, + GoogleDriveConfig, + GoogleDriveConfigSyncScope, + GranolaConfig, + ListRequestLimit, + ListRequestPage, + ListResponse, + ListSort, + ListType, + Memory, + MemoryRelation, + MemorySystem, + MetadataValue, + Namespace, + NamespaceDeleted, + NamespaceDetails, + NamespaceSystem, + Organization, + Pagination, + Profile, + ProfileBuckets, + ProfileFact, + ProfileResponse, + RelatedMemories, + RelatedMemory, + RelatedMemorySystem, + S3Config, + SearchMode, + SearchRequestAttach, + SearchRequestRerank, + SearchResponse, + SearchResult, + SearchResultIncluded, + SearchResultIncludedDocument, + SearchResultSystem, + SortOrder, + TaskType, + ValidationErrorResponse, + ValidationErrorResponseErrorItem, + ValidationErrorResponseErrorItemPathItem, + WebCrawlerConfig, + ) + from .errors import ( + ForbiddenError, + PaymentRequiredError, + ServiceUnavailableError, + UnauthorizedError, + ) + from . import connectors, documents, memories, namespaces, organization, profiles + from ._default_clients import DefaultAioHttpClient, DefaultAsyncHttpxClient + from .connectors import DeleteConnectorsRequestDeleteDocuments + from .environment import SupermemoryEnvironment + from .requests import ( + BadRequestErrorBodyParams, + BatchAddItemResultParams, + BatchAddResultParams, + ChunkListItemParams, + ChunkParams, + ChunkSystemParams, + ConnectorCapabilitiesParams, + ConnectorConfigValueParams, + ConnectorDeletedParams, + ConnectorLastSyncParams, + ConnectorListParams, + ConnectorParams, + ConnectorPickerParams, + ConnectorSelectionItemParams, + ConnectorSelectionParams, + ConnectorSetupGithubParams, + ConnectorSetupGmailParams, + ConnectorSetupGoogleDriveParams, + ConnectorSetupGranolaParams, + ConnectorSetupNotionParams, + ConnectorSetupOnedriveParams, + ConnectorSetupParams, + ConnectorSetupResultParams, + ConnectorSetupS3Params, + ConnectorSetupWebCrawlerParams, + ConnectorSetup_GithubParams, + ConnectorSetup_GmailParams, + ConnectorSetup_GoogleDriveParams, + ConnectorSetup_GranolaParams, + ConnectorSetup_NotionParams, + ConnectorSetup_OnedriveParams, + ConnectorSetup_S3Params, + ConnectorSetup_WebCrawlerParams, + ConnectorSyncFailureParams, + ConnectorSyncParams, + ConnectorSyncStartedParams, + DeleteDocumentErrorParams, + DeleteDocumentsResultParams, + DocumentInputParams, + DocumentListItemParams, + DocumentParams, + DocumentRefParams, + DocumentSystemParams, + ErrorResponseParams, + FileUploadResultParams, + FilterAndParams, + FilterArrayContainsParams, + FilterExpressionParams, + FilterNumberCompareParams, + FilterOrParams, + FilterPredicateParams, + FilterStringContainsParams, + FilterStringEqualsParams, + FilterValueEqualsParams, + FilterValueEqualsValueParams, + ForgetErrorParams, + ForgetMatchParams, + ForgetResultParams, + GoogleDriveConfigParams, + GranolaConfigParams, + ListRequestLimitParams, + ListRequestPageParams, + ListResponseParams, + MemoryParams, + MemorySystemParams, + MetadataValueParams, + NamespaceDeletedParams, + NamespaceDetailsParams, + NamespaceParams, + NamespaceSystemParams, + OrganizationParams, + PaginationParams, + ProfileBucketsParams, + ProfileFactParams, + ProfileParams, + ProfileResponseParams, + RelatedMemoriesParams, + RelatedMemoryParams, + RelatedMemorySystemParams, + S3ConfigParams, + SearchRequestAttachParams, + SearchResponseParams, + SearchResultIncludedDocumentParams, + SearchResultIncludedParams, + SearchResultParams, + SearchResultSystemParams, + ValidationErrorResponseErrorItemParams, + ValidationErrorResponseErrorItemPathItemParams, + ValidationErrorResponseParams, + WebCrawlerConfigParams, + ) +_dynamic_imports: typing.Dict[str, str] = { + "BadRequestErrorBody": ".types", + "BadRequestErrorBodyParams": ".requests", + "BatchAddItemResult": ".types", + "BatchAddItemResultParams": ".requests", + "BatchAddResult": ".types", + "BatchAddResultParams": ".requests", + "Chunk": ".types", + "ChunkListItem": ".types", + "ChunkListItemParams": ".requests", + "ChunkParams": ".requests", + "ChunkSystem": ".types", + "ChunkSystemParams": ".requests", + "Connector": ".types", + "ConnectorCapabilities": ".types", + "ConnectorCapabilitiesParams": ".requests", + "ConnectorConfigValue": ".types", + "ConnectorConfigValueParams": ".requests", + "ConnectorDeleted": ".types", + "ConnectorDeletedParams": ".requests", + "ConnectorErrorCode": ".types", + "ConnectorLastSync": ".types", + "ConnectorLastSyncParams": ".requests", + "ConnectorList": ".types", + "ConnectorListParams": ".requests", + "ConnectorParams": ".requests", + "ConnectorPicker": ".types", + "ConnectorPickerParams": ".requests", + "ConnectorProvider": ".types", + "ConnectorSelection": ".types", + "ConnectorSelectionItem": ".types", + "ConnectorSelectionItemParams": ".requests", + "ConnectorSelectionParams": ".requests", + "ConnectorSetup": ".types", + "ConnectorSetupGithub": ".types", + "ConnectorSetupGithubParams": ".requests", + "ConnectorSetupGmail": ".types", + "ConnectorSetupGmailParams": ".requests", + "ConnectorSetupGoogleDrive": ".types", + "ConnectorSetupGoogleDriveParams": ".requests", + "ConnectorSetupGranola": ".types", + "ConnectorSetupGranolaParams": ".requests", + "ConnectorSetupNotion": ".types", + "ConnectorSetupNotionParams": ".requests", + "ConnectorSetupOnedrive": ".types", + "ConnectorSetupOnedriveParams": ".requests", + "ConnectorSetupParams": ".requests", + "ConnectorSetupResult": ".types", + "ConnectorSetupResultParams": ".requests", + "ConnectorSetupS3": ".types", + "ConnectorSetupS3Params": ".requests", + "ConnectorSetupWebCrawler": ".types", + "ConnectorSetupWebCrawlerParams": ".requests", + "ConnectorSetup_Github": ".types", + "ConnectorSetup_GithubParams": ".requests", + "ConnectorSetup_Gmail": ".types", + "ConnectorSetup_GmailParams": ".requests", + "ConnectorSetup_GoogleDrive": ".types", + "ConnectorSetup_GoogleDriveParams": ".requests", + "ConnectorSetup_Granola": ".types", + "ConnectorSetup_GranolaParams": ".requests", + "ConnectorSetup_Notion": ".types", + "ConnectorSetup_NotionParams": ".requests", + "ConnectorSetup_Onedrive": ".types", + "ConnectorSetup_OnedriveParams": ".requests", + "ConnectorSetup_S3": ".types", + "ConnectorSetup_S3Params": ".requests", + "ConnectorSetup_WebCrawler": ".types", + "ConnectorSetup_WebCrawlerParams": ".requests", + "ConnectorStatus": ".types", + "ConnectorSync": ".types", + "ConnectorSyncFailure": ".types", + "ConnectorSyncFailureParams": ".requests", + "ConnectorSyncParams": ".requests", + "ConnectorSyncStarted": ".types", + "ConnectorSyncStartedParams": ".requests", + "ConnectorSyncStatus": ".types", + "ConnectorSyncTrigger": ".types", + "DefaultAioHttpClient": "._default_clients", + "DefaultAsyncHttpxClient": "._default_clients", + "DeleteConnectorsRequestDeleteDocuments": ".connectors", + "DeleteDocumentError": ".types", + "DeleteDocumentErrorParams": ".requests", + "DeleteDocumentsResult": ".types", + "DeleteDocumentsResultParams": ".requests", + "Document": ".types", + "DocumentAttachment": ".types", + "DocumentInput": ".types", + "DocumentInputParams": ".requests", + "DocumentListItem": ".types", + "DocumentListItemParams": ".requests", + "DocumentParams": ".requests", + "DocumentRef": ".types", + "DocumentRefParams": ".requests", + "DocumentSystem": ".types", + "DocumentSystemParams": ".requests", + "DreamingMode": ".types", + "ErrorResponse": ".types", + "ErrorResponseParams": ".requests", + "FileType": ".types", + "FileUploadResult": ".types", + "FileUploadResultParams": ".requests", + "FilterAnd": ".types", + "FilterAndParams": ".requests", + "FilterArrayContains": ".types", + "FilterArrayContainsOperator": ".types", + "FilterArrayContainsParams": ".requests", + "FilterExpression": ".types", + "FilterExpressionParams": ".requests", + "FilterNumberCompare": ".types", + "FilterNumberCompareOperator": ".types", + "FilterNumberCompareParams": ".requests", + "FilterOr": ".types", + "FilterOrParams": ".requests", + "FilterPredicate": ".types", + "FilterPredicateParams": ".requests", + "FilterStringContains": ".types", + "FilterStringContainsOperator": ".types", + "FilterStringContainsParams": ".requests", + "FilterStringEquals": ".types", + "FilterStringEqualsOperator": ".types", + "FilterStringEqualsParams": ".requests", + "FilterValueEquals": ".types", + "FilterValueEqualsOperator": ".types", + "FilterValueEqualsParams": ".requests", + "FilterValueEqualsValue": ".types", + "FilterValueEqualsValueParams": ".requests", + "ForbiddenError": ".errors", + "ForgetError": ".types", + "ForgetErrorParams": ".requests", + "ForgetMatch": ".types", + "ForgetMatchParams": ".requests", + "ForgetResult": ".types", + "ForgetResultParams": ".requests", + "GoogleDriveConfig": ".types", + "GoogleDriveConfigParams": ".requests", + "GoogleDriveConfigSyncScope": ".types", + "GranolaConfig": ".types", + "GranolaConfigParams": ".requests", + "ListRequestLimit": ".types", + "ListRequestLimitParams": ".requests", + "ListRequestPage": ".types", + "ListRequestPageParams": ".requests", + "ListResponse": ".types", + "ListResponseParams": ".requests", + "ListSort": ".types", + "ListType": ".types", + "Memory": ".types", + "MemoryParams": ".requests", + "MemoryRelation": ".types", + "MemorySystem": ".types", + "MemorySystemParams": ".requests", + "MetadataValue": ".types", + "MetadataValueParams": ".requests", + "Namespace": ".types", + "NamespaceDeleted": ".types", + "NamespaceDeletedParams": ".requests", + "NamespaceDetails": ".types", + "NamespaceDetailsParams": ".requests", + "NamespaceParams": ".requests", + "NamespaceSystem": ".types", + "NamespaceSystemParams": ".requests", + "Organization": ".types", + "OrganizationParams": ".requests", + "Pagination": ".types", + "PaginationParams": ".requests", + "PaymentRequiredError": ".errors", + "Profile": ".types", + "ProfileBuckets": ".types", + "ProfileBucketsParams": ".requests", + "ProfileFact": ".types", + "ProfileFactParams": ".requests", + "ProfileParams": ".requests", + "ProfileResponse": ".types", + "ProfileResponseParams": ".requests", + "RelatedMemories": ".types", + "RelatedMemoriesParams": ".requests", + "RelatedMemory": ".types", + "RelatedMemoryParams": ".requests", + "RelatedMemorySystem": ".types", + "RelatedMemorySystemParams": ".requests", + "S3Config": ".types", + "S3ConfigParams": ".requests", + "SearchMode": ".types", + "SearchRequestAttach": ".types", + "SearchRequestAttachParams": ".requests", + "SearchRequestRerank": ".types", + "SearchResponse": ".types", + "SearchResponseParams": ".requests", + "SearchResult": ".types", + "SearchResultIncluded": ".types", + "SearchResultIncludedDocument": ".types", + "SearchResultIncludedDocumentParams": ".requests", + "SearchResultIncludedParams": ".requests", + "SearchResultParams": ".requests", + "SearchResultSystem": ".types", + "SearchResultSystemParams": ".requests", + "ServiceUnavailableError": ".errors", + "SortOrder": ".types", + "SupermemoryEnvironment": ".environment", + "TaskType": ".types", + "UnauthorizedError": ".errors", + "ValidationErrorResponse": ".types", + "ValidationErrorResponseErrorItem": ".types", + "ValidationErrorResponseErrorItemParams": ".requests", + "ValidationErrorResponseErrorItemPathItem": ".types", + "ValidationErrorResponseErrorItemPathItemParams": ".requests", + "ValidationErrorResponseParams": ".requests", + "WebCrawlerConfig": ".types", + "WebCrawlerConfigParams": ".requests", + "connectors": ".connectors", + "documents": ".documents", + "memories": ".memories", + "namespaces": ".namespaces", + "organization": ".organization", + "profiles": ".profiles", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) -from . import types -from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes, omit, not_given -from ._utils import file_from_path -from ._client import ( - Client, - Stream, - Timeout, - Transport, - AsyncClient, - AsyncStream, - Supermemory, - RequestOptions, - AsyncSupermemory, -) -from ._models import BaseModel -from ._version import __title__, __version__ -from ._response import APIResponse as APIResponse, AsyncAPIResponse as AsyncAPIResponse -from ._constants import DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES, DEFAULT_CONNECTION_LIMITS -from ._exceptions import ( - APIError, - ConflictError, - NotFoundError, - APIStatusError, - RateLimitError, - APITimeoutError, - BadRequestError, - SupermemoryError, - APIConnectionError, - AuthenticationError, - InternalServerError, - PermissionDeniedError, - UnprocessableEntityError, - APIResponseValidationError, -) -from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient -from ._utils._logs import setup_logging as _setup_logging __all__ = [ - "types", - "__version__", - "__title__", - "NoneType", - "Transport", - "ProxiesTypes", - "NotGiven", - "NOT_GIVEN", - "not_given", - "Omit", - "omit", - "SupermemoryError", - "APIError", - "APIStatusError", - "APITimeoutError", - "APIConnectionError", - "APIResponseValidationError", + "AsyncSupermemory", "BadRequestError", - "AuthenticationError", - "PermissionDeniedError", - "NotFoundError", + "BadRequestErrorBody", + "BadRequestErrorBodyParams", + "BatchAddItemResult", + "BatchAddItemResultParams", + "BatchAddResult", + "BatchAddResultParams", + "Chunk", + "ChunkListItem", + "ChunkListItemParams", + "ChunkParams", + "ChunkSystem", + "ChunkSystemParams", "ConflictError", - "UnprocessableEntityError", - "RateLimitError", + "Connector", + "ConnectorCapabilities", + "ConnectorCapabilitiesParams", + "ConnectorConfigValue", + "ConnectorConfigValueParams", + "ConnectorDeleted", + "ConnectorDeletedParams", + "ConnectorErrorCode", + "ConnectorLastSync", + "ConnectorLastSyncParams", + "ConnectorList", + "ConnectorListParams", + "ConnectorParams", + "ConnectorPicker", + "ConnectorPickerParams", + "ConnectorProvider", + "ConnectorSelection", + "ConnectorSelectionItem", + "ConnectorSelectionItemParams", + "ConnectorSelectionParams", + "ConnectorSetup", + "ConnectorSetupGithub", + "ConnectorSetupGithubParams", + "ConnectorSetupGmail", + "ConnectorSetupGmailParams", + "ConnectorSetupGoogleDrive", + "ConnectorSetupGoogleDriveParams", + "ConnectorSetupGranola", + "ConnectorSetupGranolaParams", + "ConnectorSetupNotion", + "ConnectorSetupNotionParams", + "ConnectorSetupOnedrive", + "ConnectorSetupOnedriveParams", + "ConnectorSetupParams", + "ConnectorSetupResult", + "ConnectorSetupResultParams", + "ConnectorSetupS3", + "ConnectorSetupS3Params", + "ConnectorSetupWebCrawler", + "ConnectorSetupWebCrawlerParams", + "ConnectorSetup_Github", + "ConnectorSetup_GithubParams", + "ConnectorSetup_Gmail", + "ConnectorSetup_GmailParams", + "ConnectorSetup_GoogleDrive", + "ConnectorSetup_GoogleDriveParams", + "ConnectorSetup_Granola", + "ConnectorSetup_GranolaParams", + "ConnectorSetup_Notion", + "ConnectorSetup_NotionParams", + "ConnectorSetup_Onedrive", + "ConnectorSetup_OnedriveParams", + "ConnectorSetup_S3", + "ConnectorSetup_S3Params", + "ConnectorSetup_WebCrawler", + "ConnectorSetup_WebCrawlerParams", + "ConnectorStatus", + "ConnectorSync", + "ConnectorSyncFailure", + "ConnectorSyncFailureParams", + "ConnectorSyncParams", + "ConnectorSyncStarted", + "ConnectorSyncStartedParams", + "ConnectorSyncStatus", + "ConnectorSyncTrigger", + "DefaultAioHttpClient", + "DefaultAsyncHttpxClient", + "DeleteConnectorsRequestDeleteDocuments", + "DeleteDocumentError", + "DeleteDocumentErrorParams", + "DeleteDocumentsResult", + "DeleteDocumentsResultParams", + "Document", + "DocumentAttachment", + "DocumentInput", + "DocumentInputParams", + "DocumentListItem", + "DocumentListItemParams", + "DocumentParams", + "DocumentRef", + "DocumentRefParams", + "DocumentSystem", + "DocumentSystemParams", + "DreamingMode", + "ErrorResponse", + "ErrorResponseParams", + "FileType", + "FileUploadResult", + "FileUploadResultParams", + "FilterAnd", + "FilterAndParams", + "FilterArrayContains", + "FilterArrayContainsOperator", + "FilterArrayContainsParams", + "FilterExpression", + "FilterExpressionParams", + "FilterNumberCompare", + "FilterNumberCompareOperator", + "FilterNumberCompareParams", + "FilterOr", + "FilterOrParams", + "FilterPredicate", + "FilterPredicateParams", + "FilterStringContains", + "FilterStringContainsOperator", + "FilterStringContainsParams", + "FilterStringEquals", + "FilterStringEqualsOperator", + "FilterStringEqualsParams", + "FilterValueEquals", + "FilterValueEqualsOperator", + "FilterValueEqualsParams", + "FilterValueEqualsValue", + "FilterValueEqualsValueParams", + "ForbiddenError", + "ForgetError", + "ForgetErrorParams", + "ForgetMatch", + "ForgetMatchParams", + "ForgetResult", + "ForgetResultParams", + "GoogleDriveConfig", + "GoogleDriveConfigParams", + "GoogleDriveConfigSyncScope", + "GranolaConfig", + "GranolaConfigParams", "InternalServerError", - "Timeout", - "RequestOptions", - "Client", - "AsyncClient", - "Stream", - "AsyncStream", + "ListRequestLimit", + "ListRequestLimitParams", + "ListRequestPage", + "ListRequestPageParams", + "ListResponse", + "ListResponseParams", + "ListSort", + "ListType", + "Memory", + "MemoryParams", + "MemoryRelation", + "MemorySystem", + "MemorySystemParams", + "MetadataValue", + "MetadataValueParams", + "Namespace", + "NamespaceDeleted", + "NamespaceDeletedParams", + "NamespaceDetails", + "NamespaceDetailsParams", + "NamespaceParams", + "NamespaceSystem", + "NamespaceSystemParams", + "NotFoundError", + "Organization", + "OrganizationParams", + "Pagination", + "PaginationParams", + "PaymentRequiredError", + "Profile", + "ProfileBuckets", + "ProfileBucketsParams", + "ProfileFact", + "ProfileFactParams", + "ProfileParams", + "ProfileResponse", + "ProfileResponseParams", + "RelatedMemories", + "RelatedMemoriesParams", + "RelatedMemory", + "RelatedMemoryParams", + "RelatedMemorySystem", + "RelatedMemorySystemParams", + "S3Config", + "S3ConfigParams", + "SearchMode", + "SearchRequestAttach", + "SearchRequestAttachParams", + "SearchRequestRerank", + "SearchResponse", + "SearchResponseParams", + "SearchResult", + "SearchResultIncluded", + "SearchResultIncludedDocument", + "SearchResultIncludedDocumentParams", + "SearchResultIncludedParams", + "SearchResultParams", + "SearchResultSystem", + "SearchResultSystemParams", + "ServiceUnavailableError", + "SortOrder", "Supermemory", - "AsyncSupermemory", - "file_from_path", - "BaseModel", - "DEFAULT_TIMEOUT", - "DEFAULT_MAX_RETRIES", - "DEFAULT_CONNECTION_LIMITS", - "DefaultHttpxClient", - "DefaultAsyncHttpxClient", - "DefaultAioHttpClient", + "SupermemoryEnvironment", + "TaskType", + "UnauthorizedError", + "ValidationErrorResponse", + "ValidationErrorResponseErrorItem", + "ValidationErrorResponseErrorItemParams", + "ValidationErrorResponseErrorItemPathItem", + "ValidationErrorResponseErrorItemPathItemParams", + "ValidationErrorResponseParams", + "WebCrawlerConfig", + "WebCrawlerConfigParams", + "connectors", + "documents", + "memories", + "namespaces", + "organization", + "profiles", ] +# --------------------------------------------------------------------------- +# Hand-written 3.x-compatible surface (custom/supermemory/_compat.py), appended +# by scripts/apply_custom.py, which also drops these names from the lazy-import +# tables above so `supermemory.Supermemory`, `supermemory.NotFoundError`, … are +# the compatible versions. +from importlib.metadata import version as _package_version -if not _t.TYPE_CHECKING: - from ._utils._resources_proxy import resources as resources - -_setup_logging() +from ._compat import * +from ._compat import __all__ as _compat_all -# Update the __module__ attribute for exported symbols so that -# error messages point to this module instead of the module -# it was originally defined in, e.g. -# supermemory._exceptions.NotFoundError -> supermemory.NotFoundError -__locals = locals() -for __name in __all__: - if not __name.startswith("__"): - try: - __locals[__name].__module__ = "supermemory" - except (TypeError, AttributeError): - # Some of our exported symbols are builtins which we can't set attributes for. - pass +__title__ = "supermemory" +__version__ = _package_version("supermemory") +__all__ += [name for name in _compat_all if name not in __all__] +__all__ += ["__title__", "__version__"] diff --git a/src/supermemory/_base_client.py b/src/supermemory/_base_client.py deleted file mode 100644 index 253177b1..00000000 --- a/src/supermemory/_base_client.py +++ /dev/null @@ -1,2131 +0,0 @@ -from __future__ import annotations - -import sys -import json -import time -import uuid -import email -import asyncio -import inspect -import logging -import platform -import warnings -import email.utils -from types import TracebackType -from random import random -from typing import ( - TYPE_CHECKING, - Any, - Dict, - Type, - Union, - Generic, - Mapping, - TypeVar, - Iterable, - Iterator, - Optional, - Generator, - AsyncIterator, - cast, - overload, -) -from typing_extensions import Literal, override, get_origin - -import anyio -import httpx -import distro -import pydantic -from httpx import URL -from pydantic import PrivateAttr - -from . import _exceptions -from ._qs import Querystring -from ._files import to_httpx_files, async_to_httpx_files -from ._types import ( - Body, - Omit, - Query, - Headers, - Timeout, - NotGiven, - ResponseT, - AnyMapping, - PostParser, - BinaryTypes, - RequestFiles, - HttpxSendArgs, - RequestOptions, - AsyncBinaryTypes, - HttpxRequestFiles, - ModelBuilderProtocol, - not_given, -) -from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping -from ._compat import PYDANTIC_V1, model_copy, model_dump -from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type -from ._response import ( - APIResponse, - BaseAPIResponse, - AsyncAPIResponse, - extract_response_type, -) -from ._constants import ( - DEFAULT_TIMEOUT, - MAX_RETRY_DELAY, - DEFAULT_MAX_RETRIES, - INITIAL_RETRY_DELAY, - RAW_RESPONSE_HEADER, - OVERRIDE_CAST_TO_HEADER, - DEFAULT_CONNECTION_LIMITS, -) -from ._streaming import Stream, SSEDecoder, AsyncStream, SSEBytesDecoder -from ._exceptions import ( - APIStatusError, - APITimeoutError, - APIConnectionError, - APIResponseValidationError, -) -from ._utils._json import openapi_dumps - -log: logging.Logger = logging.getLogger(__name__) - -# TODO: make base page type vars covariant -SyncPageT = TypeVar("SyncPageT", bound="BaseSyncPage[Any]") -AsyncPageT = TypeVar("AsyncPageT", bound="BaseAsyncPage[Any]") - - -_T = TypeVar("_T") -_T_co = TypeVar("_T_co", covariant=True) - -_StreamT = TypeVar("_StreamT", bound=Stream[Any]) -_AsyncStreamT = TypeVar("_AsyncStreamT", bound=AsyncStream[Any]) - -if TYPE_CHECKING: - from httpx._config import ( - DEFAULT_TIMEOUT_CONFIG, # pyright: ignore[reportPrivateImportUsage] - ) - - HTTPX_DEFAULT_TIMEOUT = DEFAULT_TIMEOUT_CONFIG -else: - try: - from httpx._config import DEFAULT_TIMEOUT_CONFIG as HTTPX_DEFAULT_TIMEOUT - except ImportError: - # taken from https://github.com/encode/httpx/blob/3ba5fe0d7ac70222590e759c31442b1cab263791/httpx/_config.py#L366 - HTTPX_DEFAULT_TIMEOUT = Timeout(5.0) - - -class PageInfo: - """Stores the necessary information to build the request to retrieve the next page. - - Either `url` or `params` must be set. - """ - - url: URL | NotGiven - params: Query | NotGiven - json: Body | NotGiven - - @overload - def __init__( - self, - *, - url: URL, - ) -> None: ... - - @overload - def __init__( - self, - *, - params: Query, - ) -> None: ... - - @overload - def __init__( - self, - *, - json: Body, - ) -> None: ... - - def __init__( - self, - *, - url: URL | NotGiven = not_given, - json: Body | NotGiven = not_given, - params: Query | NotGiven = not_given, - ) -> None: - self.url = url - self.json = json - self.params = params - - @override - def __repr__(self) -> str: - if self.url: - return f"{self.__class__.__name__}(url={self.url})" - if self.json: - return f"{self.__class__.__name__}(json={self.json})" - return f"{self.__class__.__name__}(params={self.params})" - - -class BasePage(GenericModel, Generic[_T]): - """ - Defines the core interface for pagination. - - Type Args: - ModelT: The pydantic model that represents an item in the response. - - Methods: - has_next_page(): Check if there is another page available - next_page_info(): Get the necessary information to make a request for the next page - """ - - _options: FinalRequestOptions = PrivateAttr() - _model: Type[_T] = PrivateAttr() - - def has_next_page(self) -> bool: - items = self._get_page_items() - if not items: - return False - return self.next_page_info() is not None - - def next_page_info(self) -> Optional[PageInfo]: ... - - def _get_page_items(self) -> Iterable[_T]: # type: ignore[empty-body] - ... - - def _params_from_url(self, url: URL) -> httpx.QueryParams: - # TODO: do we have to preprocess params here? - return httpx.QueryParams(cast(Any, self._options.params)).merge(url.params) - - def _info_to_options(self, info: PageInfo) -> FinalRequestOptions: - options = model_copy(self._options) - options._strip_raw_response_header() - - if not isinstance(info.params, NotGiven): - options.params = {**options.params, **info.params} - return options - - if not isinstance(info.url, NotGiven): - params = self._params_from_url(info.url) - url = info.url.copy_with(params=params) - options.params = dict(url.params) - options.url = str(url) - return options - - if not isinstance(info.json, NotGiven): - if not is_mapping(info.json): - raise TypeError("Pagination is only supported with mappings") - - if not options.json_data: - options.json_data = {**info.json} - else: - if not is_mapping(options.json_data): - raise TypeError("Pagination is only supported with mappings") - - options.json_data = {**options.json_data, **info.json} - return options - - raise ValueError("Unexpected PageInfo state") - - -class BaseSyncPage(BasePage[_T], Generic[_T]): - _client: SyncAPIClient = pydantic.PrivateAttr() - - def _set_private_attributes( - self, - client: SyncAPIClient, - model: Type[_T], - options: FinalRequestOptions, - ) -> None: - if (not PYDANTIC_V1) and getattr(self, "__pydantic_private__", None) is None: - self.__pydantic_private__ = {} - - self._model = model - self._client = client - self._options = options - - # Pydantic uses a custom `__iter__` method to support casting BaseModels - # to dictionaries. e.g. dict(model). - # As we want to support `for item in page`, this is inherently incompatible - # with the default pydantic behaviour. It is not possible to support both - # use cases at once. Fortunately, this is not a big deal as all other pydantic - # methods should continue to work as expected as there is an alternative method - # to cast a model to a dictionary, model.dict(), which is used internally - # by pydantic. - def __iter__(self) -> Iterator[_T]: # type: ignore - for page in self.iter_pages(): - for item in page._get_page_items(): - yield item - - def iter_pages(self: SyncPageT) -> Iterator[SyncPageT]: - page = self - while True: - yield page - if page.has_next_page(): - page = page.get_next_page() - else: - return - - def get_next_page(self: SyncPageT) -> SyncPageT: - info = self.next_page_info() - if not info: - raise RuntimeError( - "No next page expected; please check `.has_next_page()` before calling `.get_next_page()`." - ) - - options = self._info_to_options(info) - return self._client._request_api_list(self._model, page=self.__class__, options=options) - - -class AsyncPaginator(Generic[_T, AsyncPageT]): - def __init__( - self, - client: AsyncAPIClient, - options: FinalRequestOptions, - page_cls: Type[AsyncPageT], - model: Type[_T], - ) -> None: - self._model = model - self._client = client - self._options = options - self._page_cls = page_cls - - def __await__(self) -> Generator[Any, None, AsyncPageT]: - return self._get_page().__await__() - - async def _get_page(self) -> AsyncPageT: - def _parser(resp: AsyncPageT) -> AsyncPageT: - resp._set_private_attributes( - model=self._model, - options=self._options, - client=self._client, - ) - return resp - - self._options.post_parser = _parser - - return await self._client.request(self._page_cls, self._options) - - async def __aiter__(self) -> AsyncIterator[_T]: - # https://github.com/microsoft/pyright/issues/3464 - page = cast( - AsyncPageT, - await self, # type: ignore - ) - async for item in page: - yield item - - -class BaseAsyncPage(BasePage[_T], Generic[_T]): - _client: AsyncAPIClient = pydantic.PrivateAttr() - - def _set_private_attributes( - self, - model: Type[_T], - client: AsyncAPIClient, - options: FinalRequestOptions, - ) -> None: - if (not PYDANTIC_V1) and getattr(self, "__pydantic_private__", None) is None: - self.__pydantic_private__ = {} - - self._model = model - self._client = client - self._options = options - - async def __aiter__(self) -> AsyncIterator[_T]: - async for page in self.iter_pages(): - for item in page._get_page_items(): - yield item - - async def iter_pages(self: AsyncPageT) -> AsyncIterator[AsyncPageT]: - page = self - while True: - yield page - if page.has_next_page(): - page = await page.get_next_page() - else: - return - - async def get_next_page(self: AsyncPageT) -> AsyncPageT: - info = self.next_page_info() - if not info: - raise RuntimeError( - "No next page expected; please check `.has_next_page()` before calling `.get_next_page()`." - ) - - options = self._info_to_options(info) - return await self._client._request_api_list(self._model, page=self.__class__, options=options) - - -_HttpxClientT = TypeVar("_HttpxClientT", bound=Union[httpx.Client, httpx.AsyncClient]) -_DefaultStreamT = TypeVar("_DefaultStreamT", bound=Union[Stream[Any], AsyncStream[Any]]) - - -class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]): - _client: _HttpxClientT - _version: str - _base_url: URL - max_retries: int - timeout: Union[float, Timeout, None] - _strict_response_validation: bool - _idempotency_header: str | None - _default_stream_cls: type[_DefaultStreamT] | None = None - - def __init__( - self, - *, - version: str, - base_url: str | URL, - _strict_response_validation: bool, - max_retries: int = DEFAULT_MAX_RETRIES, - timeout: float | Timeout | None = DEFAULT_TIMEOUT, - custom_headers: Mapping[str, str] | None = None, - custom_query: Mapping[str, object] | None = None, - ) -> None: - self._version = version - self._base_url = self._enforce_trailing_slash(URL(base_url)) - self.max_retries = max_retries - self.timeout = timeout - self._custom_headers = custom_headers or {} - self._custom_query = custom_query or {} - self._strict_response_validation = _strict_response_validation - self._idempotency_header = None - self._platform: Platform | None = None - - if max_retries is None: # pyright: ignore[reportUnnecessaryComparison] - raise TypeError( - "max_retries cannot be None. If you want to disable retries, pass `0`; if you want unlimited retries, pass `math.inf` or a very high number; if you want the default behavior, pass `supermemory.DEFAULT_MAX_RETRIES`" - ) - - def _enforce_trailing_slash(self, url: URL) -> URL: - if url.raw_path.endswith(b"/"): - return url - return url.copy_with(raw_path=url.raw_path + b"/") - - def _make_status_error_from_response( - self, - response: httpx.Response, - ) -> APIStatusError: - if response.is_closed and not response.is_stream_consumed: - # We can't read the response body as it has been closed - # before it was read. This can happen if an event hook - # raises a status error. - body = None - err_msg = f"Error code: {response.status_code}" - else: - err_text = response.text.strip() - body = err_text - - try: - body = json.loads(err_text) - err_msg = f"Error code: {response.status_code} - {body}" - except Exception: - err_msg = err_text or f"Error code: {response.status_code}" - - return self._make_status_error(err_msg, body=body, response=response) - - def _make_status_error( - self, - err_msg: str, - *, - body: object, - response: httpx.Response, - ) -> _exceptions.APIStatusError: - raise NotImplementedError() - - def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0) -> httpx.Headers: - custom_headers = options.headers or {} - headers_dict = _merge_mappings(self.default_headers, custom_headers) - self._validate_headers(headers_dict, custom_headers) - - # headers are case-insensitive while dictionaries are not. - headers = httpx.Headers(headers_dict) - - idempotency_header = self._idempotency_header - if idempotency_header and options.idempotency_key and idempotency_header not in headers: - headers[idempotency_header] = options.idempotency_key - - # Don't set these headers if they were already set or removed by the caller. We check - # `custom_headers`, which can contain `Omit()`, instead of `headers` to account for the removal case. - lower_custom_headers = [header.lower() for header in custom_headers] - if "x-stainless-retry-count" not in lower_custom_headers: - headers["x-stainless-retry-count"] = str(retries_taken) - if "x-stainless-read-timeout" not in lower_custom_headers: - timeout = self.timeout if isinstance(options.timeout, NotGiven) else options.timeout - if isinstance(timeout, Timeout): - timeout = timeout.read - if timeout is not None: - headers["x-stainless-read-timeout"] = str(timeout) - - return headers - - def _prepare_url(self, url: str) -> URL: - """ - Merge a URL argument together with any 'base_url' on the client, - to create the URL used for the outgoing request. - """ - # Copied from httpx's `_merge_url` method. - merge_url = URL(url) - if merge_url.is_relative_url: - merge_raw_path = self.base_url.raw_path + merge_url.raw_path.lstrip(b"/") - return self.base_url.copy_with(raw_path=merge_raw_path) - - return merge_url - - def _make_sse_decoder(self) -> SSEDecoder | SSEBytesDecoder: - return SSEDecoder() - - def _build_request( - self, - options: FinalRequestOptions, - *, - retries_taken: int = 0, - ) -> httpx.Request: - if log.isEnabledFor(logging.DEBUG): - log.debug( - "Request options: %s", - model_dump( - options, - exclude_unset=True, - # Pydantic v1 can't dump every type we support in content, so we exclude it for now. - exclude={ - "content", - } - if PYDANTIC_V1 - else {}, - ), - ) - kwargs: dict[str, Any] = {} - - json_data = options.json_data - if options.extra_json is not None: - if json_data is None: - json_data = cast(Body, options.extra_json) - elif is_mapping(json_data): - json_data = _merge_mappings(json_data, options.extra_json) - else: - raise RuntimeError(f"Unexpected JSON data type, {type(json_data)}, cannot merge with `extra_body`") - - headers = self._build_headers(options, retries_taken=retries_taken) - params = _merge_mappings(self.default_query, options.params) - content_type = headers.get("Content-Type") - files = options.files - - # If the given Content-Type header is multipart/form-data then it - # has to be removed so that httpx can generate the header with - # additional information for us as it has to be in this form - # for the server to be able to correctly parse the request: - # multipart/form-data; boundary=---abc-- - if content_type is not None and content_type.startswith("multipart/form-data"): - if "boundary" not in content_type: - # only remove the header if the boundary hasn't been explicitly set - # as the caller doesn't want httpx to come up with their own boundary - headers.pop("Content-Type") - - # As we are now sending multipart/form-data instead of application/json - # we need to tell httpx to use it, https://www.python-httpx.org/advanced/clients/#multipart-file-encoding - if json_data: - if not is_dict(json_data): - raise TypeError( - f"Expected query input to be a dictionary for multipart requests but got {type(json_data)} instead." - ) - kwargs["data"] = self._serialize_multipartform(json_data) - - # httpx determines whether or not to send a "multipart/form-data" - # request based on the truthiness of the "files" argument. - # This gets around that issue by generating a dict value that - # evaluates to true. - # - # https://github.com/encode/httpx/discussions/2399#discussioncomment-3814186 - if not files: - files = cast(HttpxRequestFiles, ForceMultipartDict()) - - prepared_url = self._prepare_url(options.url) - # preserve hard-coded query params from the url - if params and prepared_url.query: - params = {**dict(prepared_url.params.items()), **params} - prepared_url = prepared_url.copy_with(raw_path=prepared_url.raw_path.split(b"?", 1)[0]) - if "_" in prepared_url.host: - # work around https://github.com/encode/httpx/discussions/2880 - kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} - - is_body_allowed = options.method.lower() != "get" - - if is_body_allowed: - if options.content is not None and json_data is not None: - raise TypeError("Passing both `content` and `json_data` is not supported") - if options.content is not None and files is not None: - raise TypeError("Passing both `content` and `files` is not supported") - if options.content is not None: - kwargs["content"] = options.content - elif isinstance(json_data, bytes): - kwargs["content"] = json_data - elif not files: - # Don't set content when JSON is sent as multipart/form-data, - # since httpx's content param overrides other body arguments - kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None - kwargs["files"] = files - else: - headers.pop("Content-Type", None) - kwargs.pop("data", None) - - # TODO: report this error to httpx - return self._client.build_request( # pyright: ignore[reportUnknownMemberType] - headers=headers, - timeout=self.timeout if isinstance(options.timeout, NotGiven) else options.timeout, - method=options.method, - url=prepared_url, - # the `Query` type that we use is incompatible with qs' - # `Params` type as it needs to be typed as `Mapping[str, object]` - # so that passing a `TypedDict` doesn't cause an error. - # https://github.com/microsoft/pyright/issues/3526#event-6715453066 - params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None, - **kwargs, - ) - - def _serialize_multipartform(self, data: Mapping[object, object]) -> dict[str, object]: - items = self.qs.stringify_items( - # TODO: type ignore is required as stringify_items is well typed but we can't be - # well typed without heavy validation. - data, # type: ignore - array_format="brackets", - ) - serialized: dict[str, object] = {} - for key, value in items: - existing = serialized.get(key) - - if not existing: - serialized[key] = value - continue - - # If a value has already been set for this key then that - # means we're sending data like `array[]=[1, 2, 3]` and we - # need to tell httpx that we want to send multiple values with - # the same key which is done by using a list or a tuple. - # - # Note: 2d arrays should never result in the same key at both - # levels so it's safe to assume that if the value is a list, - # it was because we changed it to be a list. - if is_list(existing): - existing.append(value) - else: - serialized[key] = [existing, value] - - return serialized - - def _maybe_override_cast_to(self, cast_to: type[ResponseT], options: FinalRequestOptions) -> type[ResponseT]: - if not is_given(options.headers): - return cast_to - - # make a copy of the headers so we don't mutate user-input - headers = dict(options.headers) - - # we internally support defining a temporary header to override the - # default `cast_to` type for use with `.with_raw_response` and `.with_streaming_response` - # see _response.py for implementation details - override_cast_to = headers.pop(OVERRIDE_CAST_TO_HEADER, not_given) - if is_given(override_cast_to): - options.headers = headers - return cast(Type[ResponseT], override_cast_to) - - return cast_to - - def _should_stream_response_body(self, request: httpx.Request) -> bool: - return request.headers.get(RAW_RESPONSE_HEADER) == "stream" # type: ignore[no-any-return] - - def _process_response_data( - self, - *, - data: object, - cast_to: type[ResponseT], - response: httpx.Response, - ) -> ResponseT: - if data is None: - return cast(ResponseT, None) - - if cast_to is object: - return cast(ResponseT, data) - - try: - if inspect.isclass(cast_to) and issubclass(cast_to, ModelBuilderProtocol): - return cast(ResponseT, cast_to.build(response=response, data=data)) - - if self._strict_response_validation: - return cast(ResponseT, validate_type(type_=cast_to, value=data)) - - return cast(ResponseT, construct_type(type_=cast_to, value=data)) - except pydantic.ValidationError as err: - raise APIResponseValidationError(response=response, body=data) from err - - @property - def qs(self) -> Querystring: - return Querystring() - - @property - def custom_auth(self) -> httpx.Auth | None: - return None - - @property - def auth_headers(self) -> dict[str, str]: - return {} - - @property - def default_headers(self) -> dict[str, str | Omit]: - return { - "Accept": "application/json", - "Content-Type": "application/json", - "User-Agent": self.user_agent, - **self.platform_headers(), - **self.auth_headers, - **self._custom_headers, - } - - @property - def default_query(self) -> dict[str, object]: - return { - **self._custom_query, - } - - def _validate_headers( - self, - headers: Headers, # noqa: ARG002 - custom_headers: Headers, # noqa: ARG002 - ) -> None: - """Validate the given default headers and custom headers. - - Does nothing by default. - """ - return - - @property - def user_agent(self) -> str: - return f"{self.__class__.__name__}/Python {self._version}" - - @property - def base_url(self) -> URL: - return self._base_url - - @base_url.setter - def base_url(self, url: URL | str) -> None: - self._base_url = self._enforce_trailing_slash(url if isinstance(url, URL) else URL(url)) - - def platform_headers(self) -> Dict[str, str]: - # the actual implementation is in a separate `lru_cache` decorated - # function because adding `lru_cache` to methods will leak memory - # https://github.com/python/cpython/issues/88476 - return platform_headers(self._version, platform=self._platform) - - def _parse_retry_after_header(self, response_headers: Optional[httpx.Headers] = None) -> float | None: - """Returns a float of the number of seconds (not milliseconds) to wait after retrying, or None if unspecified. - - About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After - See also https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After#syntax - """ - if response_headers is None: - return None - - # First, try the non-standard `retry-after-ms` header for milliseconds, - # which is more precise than integer-seconds `retry-after` - try: - retry_ms_header = response_headers.get("retry-after-ms", None) - return float(retry_ms_header) / 1000 - except (TypeError, ValueError): - pass - - # Next, try parsing `retry-after` header as seconds (allowing nonstandard floats). - retry_header = response_headers.get("retry-after") - try: - # note: the spec indicates that this should only ever be an integer - # but if someone sends a float there's no reason for us to not respect it - return float(retry_header) - except (TypeError, ValueError): - pass - - # Last, try parsing `retry-after` as a date. - retry_date_tuple = email.utils.parsedate_tz(retry_header) - if retry_date_tuple is None: - return None - - retry_date = email.utils.mktime_tz(retry_date_tuple) - return float(retry_date - time.time()) - - def _calculate_retry_timeout( - self, - remaining_retries: int, - options: FinalRequestOptions, - response_headers: Optional[httpx.Headers] = None, - ) -> float: - max_retries = options.get_max_retries(self.max_retries) - - # If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says. - retry_after = self._parse_retry_after_header(response_headers) - if retry_after is not None and 0 < retry_after <= 60: - return retry_after - - # Also cap retry count to 1000 to avoid any potential overflows with `pow` - nb_retries = min(max_retries - remaining_retries, 1000) - - # Apply exponential backoff, but not more than the max. - sleep_seconds = min(INITIAL_RETRY_DELAY * pow(2.0, nb_retries), MAX_RETRY_DELAY) - - # Apply some jitter, plus-or-minus half a second. - jitter = 1 - 0.25 * random() - timeout = sleep_seconds * jitter - return timeout if timeout >= 0 else 0 - - def _should_retry(self, response: httpx.Response) -> bool: - # Note: this is not a standard header - should_retry_header = response.headers.get("x-should-retry") - - # If the server explicitly says whether or not to retry, obey. - if should_retry_header == "true": - log.debug("Retrying as header `x-should-retry` is set to `true`") - return True - if should_retry_header == "false": - log.debug("Not retrying as header `x-should-retry` is set to `false`") - return False - - # Retry on request timeouts. - if response.status_code == 408: - log.debug("Retrying due to status code %i", response.status_code) - return True - - # Retry on lock timeouts. - if response.status_code == 409: - log.debug("Retrying due to status code %i", response.status_code) - return True - - # Retry on rate limits. - if response.status_code == 429: - log.debug("Retrying due to status code %i", response.status_code) - return True - - # Retry internal errors. - if response.status_code >= 500: - log.debug("Retrying due to status code %i", response.status_code) - return True - - log.debug("Not retrying") - return False - - def _idempotency_key(self) -> str: - return f"stainless-python-retry-{uuid.uuid4()}" - - -class _DefaultHttpxClient(httpx.Client): - def __init__(self, **kwargs: Any) -> None: - kwargs.setdefault("timeout", DEFAULT_TIMEOUT) - kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) - kwargs.setdefault("follow_redirects", True) - super().__init__(**kwargs) - - -if TYPE_CHECKING: - DefaultHttpxClient = httpx.Client - """An alias to `httpx.Client` that provides the same defaults that this SDK - uses internally. - - This is useful because overriding the `http_client` with your own instance of - `httpx.Client` will result in httpx's defaults being used, not ours. - """ -else: - DefaultHttpxClient = _DefaultHttpxClient - - -class SyncHttpxClientWrapper(DefaultHttpxClient): - def __del__(self) -> None: - if self.is_closed: - return - - try: - self.close() - except Exception: - pass - - -class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]): - _client: httpx.Client - _default_stream_cls: type[Stream[Any]] | None = None - - def __init__( - self, - *, - version: str, - base_url: str | URL, - max_retries: int = DEFAULT_MAX_RETRIES, - timeout: float | Timeout | None | NotGiven = not_given, - http_client: httpx.Client | None = None, - custom_headers: Mapping[str, str] | None = None, - custom_query: Mapping[str, object] | None = None, - _strict_response_validation: bool, - ) -> None: - if not is_given(timeout): - # if the user passed in a custom http client with a non-default - # timeout set then we use that timeout. - # - # note: there is an edge case here where the user passes in a client - # where they've explicitly set the timeout to match the default timeout - # as this check is structural, meaning that we'll think they didn't - # pass in a timeout and will ignore it - if http_client and http_client.timeout != HTTPX_DEFAULT_TIMEOUT: - timeout = http_client.timeout - else: - timeout = DEFAULT_TIMEOUT - - if http_client is not None and not isinstance(http_client, httpx.Client): # pyright: ignore[reportUnnecessaryIsInstance] - raise TypeError( - f"Invalid `http_client` argument; Expected an instance of `httpx.Client` but got {type(http_client)}" - ) - - super().__init__( - version=version, - # cast to a valid type because mypy doesn't understand our type narrowing - timeout=cast(Timeout, timeout), - base_url=base_url, - max_retries=max_retries, - custom_query=custom_query, - custom_headers=custom_headers, - _strict_response_validation=_strict_response_validation, - ) - self._client = http_client or SyncHttpxClientWrapper( - base_url=base_url, - # cast to a valid type because mypy doesn't understand our type narrowing - timeout=cast(Timeout, timeout), - ) - - def is_closed(self) -> bool: - return self._client.is_closed - - def close(self) -> None: - """Close the underlying HTTPX client. - - The client will *not* be usable after this. - """ - # If an error is thrown while constructing a client, self._client - # may not be present - if hasattr(self, "_client"): - self._client.close() - - def __enter__(self: _T) -> _T: - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - self.close() - - def _prepare_options( - self, - options: FinalRequestOptions, # noqa: ARG002 - ) -> FinalRequestOptions: - """Hook for mutating the given options""" - return options - - def _prepare_request( - self, - request: httpx.Request, # noqa: ARG002 - ) -> None: - """This method is used as a callback for mutating the `Request` object - after it has been constructed. - This is useful for cases where you want to add certain headers based off of - the request properties, e.g. `url`, `method` etc. - """ - return None - - @overload - def request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: Literal[True], - stream_cls: Type[_StreamT], - ) -> _StreamT: ... - - @overload - def request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: Literal[False] = False, - ) -> ResponseT: ... - - @overload - def request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: bool = False, - stream_cls: Type[_StreamT] | None = None, - ) -> ResponseT | _StreamT: ... - - def request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: bool = False, - stream_cls: type[_StreamT] | None = None, - ) -> ResponseT | _StreamT: - cast_to = self._maybe_override_cast_to(cast_to, options) - - # create a copy of the options we were given so that if the - # options are mutated later & we then retry, the retries are - # given the original options - input_options = model_copy(options) - if input_options.idempotency_key is None and input_options.method.lower() != "get": - # ensure the idempotency key is reused between requests - input_options.idempotency_key = self._idempotency_key() - - response: httpx.Response | None = None - max_retries = input_options.get_max_retries(self.max_retries) - - retries_taken = 0 - for retries_taken in range(max_retries + 1): - options = model_copy(input_options) - options = self._prepare_options(options) - - remaining_retries = max_retries - retries_taken - request = self._build_request(options, retries_taken=retries_taken) - self._prepare_request(request) - - kwargs: HttpxSendArgs = {} - if self.custom_auth is not None: - kwargs["auth"] = self.custom_auth - - if options.follow_redirects is not None: - kwargs["follow_redirects"] = options.follow_redirects - - log.debug("Sending HTTP Request: %s %s", request.method, request.url) - - response = None - try: - response = self._client.send( - request, - stream=stream or self._should_stream_response_body(request=request), - **kwargs, - ) - except httpx.TimeoutException as err: - log.debug("Encountered httpx.TimeoutException", exc_info=True) - - if remaining_retries > 0: - self._sleep_for_retry( - retries_taken=retries_taken, - max_retries=max_retries, - options=input_options, - response=None, - ) - continue - - log.debug("Raising timeout error") - raise APITimeoutError(request=request) from err - except Exception as err: - log.debug("Encountered Exception", exc_info=True) - - if remaining_retries > 0: - self._sleep_for_retry( - retries_taken=retries_taken, - max_retries=max_retries, - options=input_options, - response=None, - ) - continue - - log.debug("Raising connection error") - raise APIConnectionError(request=request) from err - - log.debug( - 'HTTP Response: %s %s "%i %s" %s', - request.method, - request.url, - response.status_code, - response.reason_phrase, - response.headers, - ) - - try: - response.raise_for_status() - except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code - log.debug("Encountered httpx.HTTPStatusError", exc_info=True) - - if remaining_retries > 0 and self._should_retry(err.response): - err.response.close() - self._sleep_for_retry( - retries_taken=retries_taken, - max_retries=max_retries, - options=input_options, - response=response, - ) - continue - - # If the response is streamed then we need to explicitly read the response - # to completion before attempting to access the response text. - if not err.response.is_closed: - err.response.read() - - log.debug("Re-raising status error") - raise self._make_status_error_from_response(err.response) from None - - break - - assert response is not None, "could not resolve response (should never happen)" - return self._process_response( - cast_to=cast_to, - options=options, - response=response, - stream=stream, - stream_cls=stream_cls, - retries_taken=retries_taken, - ) - - def _sleep_for_retry( - self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None - ) -> None: - remaining_retries = max_retries - retries_taken - if remaining_retries == 1: - log.debug("1 retry left") - else: - log.debug("%i retries left", remaining_retries) - - timeout = self._calculate_retry_timeout(remaining_retries, options, response.headers if response else None) - log.info("Retrying request to %s in %f seconds", options.url, timeout) - - time.sleep(timeout) - - def _process_response( - self, - *, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - response: httpx.Response, - stream: bool, - stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, - retries_taken: int = 0, - ) -> ResponseT: - origin = get_origin(cast_to) or cast_to - - if ( - inspect.isclass(origin) - and issubclass(origin, BaseAPIResponse) - # we only want to actually return the custom BaseAPIResponse class if we're - # returning the raw response, or if we're not streaming SSE, as if we're streaming - # SSE then `cast_to` doesn't actively reflect the type we need to parse into - and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER))) - ): - if not issubclass(origin, APIResponse): - raise TypeError(f"API Response types must subclass {APIResponse}; Received {origin}") - - response_cls = cast("type[BaseAPIResponse[Any]]", cast_to) - return cast( - ResponseT, - response_cls( - raw=response, - client=self, - cast_to=extract_response_type(response_cls), - stream=stream, - stream_cls=stream_cls, - options=options, - retries_taken=retries_taken, - ), - ) - - if cast_to == httpx.Response: - return cast(ResponseT, response) - - api_response = APIResponse( - raw=response, - client=self, - cast_to=cast("type[ResponseT]", cast_to), # pyright: ignore[reportUnnecessaryCast] - stream=stream, - stream_cls=stream_cls, - options=options, - retries_taken=retries_taken, - ) - if bool(response.request.headers.get(RAW_RESPONSE_HEADER)): - return cast(ResponseT, api_response) - - return api_response.parse() - - def _request_api_list( - self, - model: Type[object], - page: Type[SyncPageT], - options: FinalRequestOptions, - ) -> SyncPageT: - def _parser(resp: SyncPageT) -> SyncPageT: - resp._set_private_attributes( - client=self, - model=model, - options=options, - ) - return resp - - options.post_parser = _parser - - return self.request(page, options, stream=False) - - @overload - def get( - self, - path: str, - *, - cast_to: Type[ResponseT], - options: RequestOptions = {}, - stream: Literal[False] = False, - ) -> ResponseT: ... - - @overload - def get( - self, - path: str, - *, - cast_to: Type[ResponseT], - options: RequestOptions = {}, - stream: Literal[True], - stream_cls: type[_StreamT], - ) -> _StreamT: ... - - @overload - def get( - self, - path: str, - *, - cast_to: Type[ResponseT], - options: RequestOptions = {}, - stream: bool, - stream_cls: type[_StreamT] | None = None, - ) -> ResponseT | _StreamT: ... - - def get( - self, - path: str, - *, - cast_to: Type[ResponseT], - options: RequestOptions = {}, - stream: bool = False, - stream_cls: type[_StreamT] | None = None, - ) -> ResponseT | _StreamT: - opts = FinalRequestOptions.construct(method="get", url=path, **options) - # cast is required because mypy complains about returning Any even though - # it understands the type variables - return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) - - @overload - def post( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - content: BinaryTypes | None = None, - options: RequestOptions = {}, - files: RequestFiles | None = None, - stream: Literal[False] = False, - ) -> ResponseT: ... - - @overload - def post( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - content: BinaryTypes | None = None, - options: RequestOptions = {}, - files: RequestFiles | None = None, - stream: Literal[True], - stream_cls: type[_StreamT], - ) -> _StreamT: ... - - @overload - def post( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - content: BinaryTypes | None = None, - options: RequestOptions = {}, - files: RequestFiles | None = None, - stream: bool, - stream_cls: type[_StreamT] | None = None, - ) -> ResponseT | _StreamT: ... - - def post( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - content: BinaryTypes | None = None, - options: RequestOptions = {}, - files: RequestFiles | None = None, - stream: bool = False, - stream_cls: type[_StreamT] | None = None, - ) -> ResponseT | _StreamT: - if body is not None and content is not None: - raise TypeError("Passing both `body` and `content` is not supported") - if files is not None and content is not None: - raise TypeError("Passing both `files` and `content` is not supported") - if isinstance(body, bytes): - warnings.warn( - "Passing raw bytes as `body` is deprecated and will be removed in a future version. " - "Please pass raw bytes via the `content` parameter instead.", - DeprecationWarning, - stacklevel=2, - ) - opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, content=content, files=to_httpx_files(files), **options - ) - return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) - - def patch( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - content: BinaryTypes | None = None, - files: RequestFiles | None = None, - options: RequestOptions = {}, - ) -> ResponseT: - if body is not None and content is not None: - raise TypeError("Passing both `body` and `content` is not supported") - if files is not None and content is not None: - raise TypeError("Passing both `files` and `content` is not supported") - if isinstance(body, bytes): - warnings.warn( - "Passing raw bytes as `body` is deprecated and will be removed in a future version. " - "Please pass raw bytes via the `content` parameter instead.", - DeprecationWarning, - stacklevel=2, - ) - opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, content=content, files=to_httpx_files(files), **options - ) - return self.request(cast_to, opts) - - def put( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - content: BinaryTypes | None = None, - files: RequestFiles | None = None, - options: RequestOptions = {}, - ) -> ResponseT: - if body is not None and content is not None: - raise TypeError("Passing both `body` and `content` is not supported") - if files is not None and content is not None: - raise TypeError("Passing both `files` and `content` is not supported") - if isinstance(body, bytes): - warnings.warn( - "Passing raw bytes as `body` is deprecated and will be removed in a future version. " - "Please pass raw bytes via the `content` parameter instead.", - DeprecationWarning, - stacklevel=2, - ) - opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, content=content, files=to_httpx_files(files), **options - ) - return self.request(cast_to, opts) - - def delete( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - content: BinaryTypes | None = None, - options: RequestOptions = {}, - ) -> ResponseT: - if body is not None and content is not None: - raise TypeError("Passing both `body` and `content` is not supported") - if isinstance(body, bytes): - warnings.warn( - "Passing raw bytes as `body` is deprecated and will be removed in a future version. " - "Please pass raw bytes via the `content` parameter instead.", - DeprecationWarning, - stacklevel=2, - ) - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) - return self.request(cast_to, opts) - - def get_api_list( - self, - path: str, - *, - model: Type[object], - page: Type[SyncPageT], - body: Body | None = None, - options: RequestOptions = {}, - method: str = "get", - ) -> SyncPageT: - opts = FinalRequestOptions.construct(method=method, url=path, json_data=body, **options) - return self._request_api_list(model, page, opts) - - -class _DefaultAsyncHttpxClient(httpx.AsyncClient): - def __init__(self, **kwargs: Any) -> None: - kwargs.setdefault("timeout", DEFAULT_TIMEOUT) - kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) - kwargs.setdefault("follow_redirects", True) - super().__init__(**kwargs) - - -try: - import httpx_aiohttp -except ImportError: - - class _DefaultAioHttpClient(httpx.AsyncClient): - def __init__(self, **_kwargs: Any) -> None: - raise RuntimeError("To use the aiohttp client you must have installed the package with the `aiohttp` extra") -else: - - class _DefaultAioHttpClient(httpx_aiohttp.HttpxAiohttpClient): # type: ignore - def __init__(self, **kwargs: Any) -> None: - kwargs.setdefault("timeout", DEFAULT_TIMEOUT) - kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) - kwargs.setdefault("follow_redirects", True) - - super().__init__(**kwargs) - - -if TYPE_CHECKING: - DefaultAsyncHttpxClient = httpx.AsyncClient - """An alias to `httpx.AsyncClient` that provides the same defaults that this SDK - uses internally. - - This is useful because overriding the `http_client` with your own instance of - `httpx.AsyncClient` will result in httpx's defaults being used, not ours. - """ - - DefaultAioHttpClient = httpx.AsyncClient - """An alias to `httpx.AsyncClient` that changes the default HTTP transport to `aiohttp`.""" -else: - DefaultAsyncHttpxClient = _DefaultAsyncHttpxClient - DefaultAioHttpClient = _DefaultAioHttpClient - - -class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient): - def __del__(self) -> None: - if self.is_closed: - return - - try: - # TODO(someday): support non asyncio runtimes here - asyncio.get_running_loop().create_task(self.aclose()) - except Exception: - pass - - -class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]): - _client: httpx.AsyncClient - _default_stream_cls: type[AsyncStream[Any]] | None = None - - def __init__( - self, - *, - version: str, - base_url: str | URL, - _strict_response_validation: bool, - max_retries: int = DEFAULT_MAX_RETRIES, - timeout: float | Timeout | None | NotGiven = not_given, - http_client: httpx.AsyncClient | None = None, - custom_headers: Mapping[str, str] | None = None, - custom_query: Mapping[str, object] | None = None, - ) -> None: - if not is_given(timeout): - # if the user passed in a custom http client with a non-default - # timeout set then we use that timeout. - # - # note: there is an edge case here where the user passes in a client - # where they've explicitly set the timeout to match the default timeout - # as this check is structural, meaning that we'll think they didn't - # pass in a timeout and will ignore it - if http_client and http_client.timeout != HTTPX_DEFAULT_TIMEOUT: - timeout = http_client.timeout - else: - timeout = DEFAULT_TIMEOUT - - if http_client is not None and not isinstance(http_client, httpx.AsyncClient): # pyright: ignore[reportUnnecessaryIsInstance] - raise TypeError( - f"Invalid `http_client` argument; Expected an instance of `httpx.AsyncClient` but got {type(http_client)}" - ) - - super().__init__( - version=version, - base_url=base_url, - # cast to a valid type because mypy doesn't understand our type narrowing - timeout=cast(Timeout, timeout), - max_retries=max_retries, - custom_query=custom_query, - custom_headers=custom_headers, - _strict_response_validation=_strict_response_validation, - ) - self._client = http_client or AsyncHttpxClientWrapper( - base_url=base_url, - # cast to a valid type because mypy doesn't understand our type narrowing - timeout=cast(Timeout, timeout), - ) - - def is_closed(self) -> bool: - return self._client.is_closed - - async def close(self) -> None: - """Close the underlying HTTPX client. - - The client will *not* be usable after this. - """ - await self._client.aclose() - - async def __aenter__(self: _T) -> _T: - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - await self.close() - - async def _prepare_options( - self, - options: FinalRequestOptions, # noqa: ARG002 - ) -> FinalRequestOptions: - """Hook for mutating the given options""" - return options - - async def _prepare_request( - self, - request: httpx.Request, # noqa: ARG002 - ) -> None: - """This method is used as a callback for mutating the `Request` object - after it has been constructed. - This is useful for cases where you want to add certain headers based off of - the request properties, e.g. `url`, `method` etc. - """ - return None - - @overload - async def request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: Literal[False] = False, - ) -> ResponseT: ... - - @overload - async def request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: Literal[True], - stream_cls: type[_AsyncStreamT], - ) -> _AsyncStreamT: ... - - @overload - async def request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: bool, - stream_cls: type[_AsyncStreamT] | None = None, - ) -> ResponseT | _AsyncStreamT: ... - - async def request( - self, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - *, - stream: bool = False, - stream_cls: type[_AsyncStreamT] | None = None, - ) -> ResponseT | _AsyncStreamT: - if self._platform is None: - # `get_platform` can make blocking IO calls so we - # execute it earlier while we are in an async context - self._platform = await asyncify(get_platform)() - - cast_to = self._maybe_override_cast_to(cast_to, options) - - # create a copy of the options we were given so that if the - # options are mutated later & we then retry, the retries are - # given the original options - input_options = model_copy(options) - if input_options.idempotency_key is None and input_options.method.lower() != "get": - # ensure the idempotency key is reused between requests - input_options.idempotency_key = self._idempotency_key() - - response: httpx.Response | None = None - max_retries = input_options.get_max_retries(self.max_retries) - - retries_taken = 0 - for retries_taken in range(max_retries + 1): - options = model_copy(input_options) - options = await self._prepare_options(options) - - remaining_retries = max_retries - retries_taken - request = self._build_request(options, retries_taken=retries_taken) - await self._prepare_request(request) - - kwargs: HttpxSendArgs = {} - if self.custom_auth is not None: - kwargs["auth"] = self.custom_auth - - if options.follow_redirects is not None: - kwargs["follow_redirects"] = options.follow_redirects - - log.debug("Sending HTTP Request: %s %s", request.method, request.url) - - response = None - try: - response = await self._client.send( - request, - stream=stream or self._should_stream_response_body(request=request), - **kwargs, - ) - except httpx.TimeoutException as err: - log.debug("Encountered httpx.TimeoutException", exc_info=True) - - if remaining_retries > 0: - await self._sleep_for_retry( - retries_taken=retries_taken, - max_retries=max_retries, - options=input_options, - response=None, - ) - continue - - log.debug("Raising timeout error") - raise APITimeoutError(request=request) from err - except Exception as err: - log.debug("Encountered Exception", exc_info=True) - - if remaining_retries > 0: - await self._sleep_for_retry( - retries_taken=retries_taken, - max_retries=max_retries, - options=input_options, - response=None, - ) - continue - - log.debug("Raising connection error") - raise APIConnectionError(request=request) from err - - log.debug( - 'HTTP Response: %s %s "%i %s" %s', - request.method, - request.url, - response.status_code, - response.reason_phrase, - response.headers, - ) - - try: - response.raise_for_status() - except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code - log.debug("Encountered httpx.HTTPStatusError", exc_info=True) - - if remaining_retries > 0 and self._should_retry(err.response): - await err.response.aclose() - await self._sleep_for_retry( - retries_taken=retries_taken, - max_retries=max_retries, - options=input_options, - response=response, - ) - continue - - # If the response is streamed then we need to explicitly read the response - # to completion before attempting to access the response text. - if not err.response.is_closed: - await err.response.aread() - - log.debug("Re-raising status error") - raise self._make_status_error_from_response(err.response) from None - - break - - assert response is not None, "could not resolve response (should never happen)" - return await self._process_response( - cast_to=cast_to, - options=options, - response=response, - stream=stream, - stream_cls=stream_cls, - retries_taken=retries_taken, - ) - - async def _sleep_for_retry( - self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None - ) -> None: - remaining_retries = max_retries - retries_taken - if remaining_retries == 1: - log.debug("1 retry left") - else: - log.debug("%i retries left", remaining_retries) - - timeout = self._calculate_retry_timeout(remaining_retries, options, response.headers if response else None) - log.info("Retrying request to %s in %f seconds", options.url, timeout) - - await anyio.sleep(timeout) - - async def _process_response( - self, - *, - cast_to: Type[ResponseT], - options: FinalRequestOptions, - response: httpx.Response, - stream: bool, - stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, - retries_taken: int = 0, - ) -> ResponseT: - origin = get_origin(cast_to) or cast_to - - if ( - inspect.isclass(origin) - and issubclass(origin, BaseAPIResponse) - # we only want to actually return the custom BaseAPIResponse class if we're - # returning the raw response, or if we're not streaming SSE, as if we're streaming - # SSE then `cast_to` doesn't actively reflect the type we need to parse into - and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER))) - ): - if not issubclass(origin, AsyncAPIResponse): - raise TypeError(f"API Response types must subclass {AsyncAPIResponse}; Received {origin}") - - response_cls = cast("type[BaseAPIResponse[Any]]", cast_to) - return cast( - "ResponseT", - response_cls( - raw=response, - client=self, - cast_to=extract_response_type(response_cls), - stream=stream, - stream_cls=stream_cls, - options=options, - retries_taken=retries_taken, - ), - ) - - if cast_to == httpx.Response: - return cast(ResponseT, response) - - api_response = AsyncAPIResponse( - raw=response, - client=self, - cast_to=cast("type[ResponseT]", cast_to), # pyright: ignore[reportUnnecessaryCast] - stream=stream, - stream_cls=stream_cls, - options=options, - retries_taken=retries_taken, - ) - if bool(response.request.headers.get(RAW_RESPONSE_HEADER)): - return cast(ResponseT, api_response) - - return await api_response.parse() - - def _request_api_list( - self, - model: Type[_T], - page: Type[AsyncPageT], - options: FinalRequestOptions, - ) -> AsyncPaginator[_T, AsyncPageT]: - return AsyncPaginator(client=self, options=options, page_cls=page, model=model) - - @overload - async def get( - self, - path: str, - *, - cast_to: Type[ResponseT], - options: RequestOptions = {}, - stream: Literal[False] = False, - ) -> ResponseT: ... - - @overload - async def get( - self, - path: str, - *, - cast_to: Type[ResponseT], - options: RequestOptions = {}, - stream: Literal[True], - stream_cls: type[_AsyncStreamT], - ) -> _AsyncStreamT: ... - - @overload - async def get( - self, - path: str, - *, - cast_to: Type[ResponseT], - options: RequestOptions = {}, - stream: bool, - stream_cls: type[_AsyncStreamT] | None = None, - ) -> ResponseT | _AsyncStreamT: ... - - async def get( - self, - path: str, - *, - cast_to: Type[ResponseT], - options: RequestOptions = {}, - stream: bool = False, - stream_cls: type[_AsyncStreamT] | None = None, - ) -> ResponseT | _AsyncStreamT: - opts = FinalRequestOptions.construct(method="get", url=path, **options) - return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) - - @overload - async def post( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - content: AsyncBinaryTypes | None = None, - files: RequestFiles | None = None, - options: RequestOptions = {}, - stream: Literal[False] = False, - ) -> ResponseT: ... - - @overload - async def post( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - content: AsyncBinaryTypes | None = None, - files: RequestFiles | None = None, - options: RequestOptions = {}, - stream: Literal[True], - stream_cls: type[_AsyncStreamT], - ) -> _AsyncStreamT: ... - - @overload - async def post( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - content: AsyncBinaryTypes | None = None, - files: RequestFiles | None = None, - options: RequestOptions = {}, - stream: bool, - stream_cls: type[_AsyncStreamT] | None = None, - ) -> ResponseT | _AsyncStreamT: ... - - async def post( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - content: AsyncBinaryTypes | None = None, - files: RequestFiles | None = None, - options: RequestOptions = {}, - stream: bool = False, - stream_cls: type[_AsyncStreamT] | None = None, - ) -> ResponseT | _AsyncStreamT: - if body is not None and content is not None: - raise TypeError("Passing both `body` and `content` is not supported") - if files is not None and content is not None: - raise TypeError("Passing both `files` and `content` is not supported") - if isinstance(body, bytes): - warnings.warn( - "Passing raw bytes as `body` is deprecated and will be removed in a future version. " - "Please pass raw bytes via the `content` parameter instead.", - DeprecationWarning, - stacklevel=2, - ) - opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options - ) - return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) - - async def patch( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - content: AsyncBinaryTypes | None = None, - files: RequestFiles | None = None, - options: RequestOptions = {}, - ) -> ResponseT: - if body is not None and content is not None: - raise TypeError("Passing both `body` and `content` is not supported") - if files is not None and content is not None: - raise TypeError("Passing both `files` and `content` is not supported") - if isinstance(body, bytes): - warnings.warn( - "Passing raw bytes as `body` is deprecated and will be removed in a future version. " - "Please pass raw bytes via the `content` parameter instead.", - DeprecationWarning, - stacklevel=2, - ) - opts = FinalRequestOptions.construct( - method="patch", - url=path, - json_data=body, - content=content, - files=await async_to_httpx_files(files), - **options, - ) - return await self.request(cast_to, opts) - - async def put( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - content: AsyncBinaryTypes | None = None, - files: RequestFiles | None = None, - options: RequestOptions = {}, - ) -> ResponseT: - if body is not None and content is not None: - raise TypeError("Passing both `body` and `content` is not supported") - if files is not None and content is not None: - raise TypeError("Passing both `files` and `content` is not supported") - if isinstance(body, bytes): - warnings.warn( - "Passing raw bytes as `body` is deprecated and will be removed in a future version. " - "Please pass raw bytes via the `content` parameter instead.", - DeprecationWarning, - stacklevel=2, - ) - opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options - ) - return await self.request(cast_to, opts) - - async def delete( - self, - path: str, - *, - cast_to: Type[ResponseT], - body: Body | None = None, - content: AsyncBinaryTypes | None = None, - options: RequestOptions = {}, - ) -> ResponseT: - if body is not None and content is not None: - raise TypeError("Passing both `body` and `content` is not supported") - if isinstance(body, bytes): - warnings.warn( - "Passing raw bytes as `body` is deprecated and will be removed in a future version. " - "Please pass raw bytes via the `content` parameter instead.", - DeprecationWarning, - stacklevel=2, - ) - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) - return await self.request(cast_to, opts) - - def get_api_list( - self, - path: str, - *, - model: Type[_T], - page: Type[AsyncPageT], - body: Body | None = None, - options: RequestOptions = {}, - method: str = "get", - ) -> AsyncPaginator[_T, AsyncPageT]: - opts = FinalRequestOptions.construct(method=method, url=path, json_data=body, **options) - return self._request_api_list(model, page, opts) - - -def make_request_options( - *, - query: Query | None = None, - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - idempotency_key: str | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - post_parser: PostParser | NotGiven = not_given, -) -> RequestOptions: - """Create a dict of type RequestOptions without keys of NotGiven values.""" - options: RequestOptions = {} - if extra_headers is not None: - options["headers"] = extra_headers - - if extra_body is not None: - options["extra_json"] = cast(AnyMapping, extra_body) - - if query is not None: - options["params"] = query - - if extra_query is not None: - options["params"] = {**options.get("params", {}), **extra_query} - - if not isinstance(timeout, NotGiven): - options["timeout"] = timeout - - if idempotency_key is not None: - options["idempotency_key"] = idempotency_key - - if is_given(post_parser): - # internal - options["post_parser"] = post_parser # type: ignore - - return options - - -class ForceMultipartDict(Dict[str, None]): - def __bool__(self) -> bool: - return True - - -class OtherPlatform: - def __init__(self, name: str) -> None: - self.name = name - - @override - def __str__(self) -> str: - return f"Other:{self.name}" - - -Platform = Union[ - OtherPlatform, - Literal[ - "MacOS", - "Linux", - "Windows", - "FreeBSD", - "OpenBSD", - "iOS", - "Android", - "Unknown", - ], -] - - -def get_platform() -> Platform: - try: - system = platform.system().lower() - platform_name = platform.platform().lower() - except Exception: - return "Unknown" - - if "iphone" in platform_name or "ipad" in platform_name: - # Tested using Python3IDE on an iPhone 11 and Pythonista on an iPad 7 - # system is Darwin and platform_name is a string like: - # - Darwin-21.6.0-iPhone12,1-64bit - # - Darwin-21.6.0-iPad7,11-64bit - return "iOS" - - if system == "darwin": - return "MacOS" - - if system == "windows": - return "Windows" - - if "android" in platform_name: - # Tested using Pydroid 3 - # system is Linux and platform_name is a string like 'Linux-5.10.81-android12-9-00001-geba40aecb3b7-ab8534902-aarch64-with-libc' - return "Android" - - if system == "linux": - # https://distro.readthedocs.io/en/latest/#distro.id - distro_id = distro.id() - if distro_id == "freebsd": - return "FreeBSD" - - if distro_id == "openbsd": - return "OpenBSD" - - return "Linux" - - if platform_name: - return OtherPlatform(platform_name) - - return "Unknown" - - -@lru_cache(maxsize=None) -def platform_headers(version: str, *, platform: Platform | None) -> Dict[str, str]: - return { - "X-Stainless-Lang": "python", - "X-Stainless-Package-Version": version, - "X-Stainless-OS": str(platform or get_platform()), - "X-Stainless-Arch": str(get_architecture()), - "X-Stainless-Runtime": get_python_runtime(), - "X-Stainless-Runtime-Version": get_python_version(), - } - - -class OtherArch: - def __init__(self, name: str) -> None: - self.name = name - - @override - def __str__(self) -> str: - return f"other:{self.name}" - - -Arch = Union[OtherArch, Literal["x32", "x64", "arm", "arm64", "unknown"]] - - -def get_python_runtime() -> str: - try: - return platform.python_implementation() - except Exception: - return "unknown" - - -def get_python_version() -> str: - try: - return platform.python_version() - except Exception: - return "unknown" - - -def get_architecture() -> Arch: - try: - machine = platform.machine().lower() - except Exception: - return "unknown" - - if machine in ("arm64", "aarch64"): - return "arm64" - - # TODO: untested - if machine == "arm": - return "arm" - - if machine == "x86_64": - return "x64" - - # TODO: untested - if sys.maxsize <= 2**32: - return "x32" - - if machine: - return OtherArch(machine) - - return "unknown" - - -def _merge_mappings( - obj1: Mapping[_T_co, Union[_T, Omit]], - obj2: Mapping[_T_co, Union[_T, Omit]], -) -> Dict[_T_co, _T]: - """Merge two mappings of the same type, removing any values that are instances of `Omit`. - - In cases with duplicate keys the second mapping takes precedence. - """ - merged = {**obj1, **obj2} - return {key: value for key, value in merged.items() if not isinstance(value, Omit)} diff --git a/src/supermemory/_client.py b/src/supermemory/_client.py deleted file mode 100644 index 2cc89e40..00000000 --- a/src/supermemory/_client.py +++ /dev/null @@ -1,994 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import TYPE_CHECKING, Any, Dict, List, Union, Mapping -from typing_extensions import Self, Literal, override - -import httpx - -from . import _exceptions -from ._qs import Querystring -from .types import client_add_params, client_profile_params -from ._types import ( - Body, - Omit, - Query, - Headers, - Timeout, - NotGiven, - Transport, - ProxiesTypes, - RequestOptions, - SequenceNotStr, - omit, - not_given, -) -from ._utils import ( - is_given, - is_mapping_t, - maybe_transform, - get_async_library, - async_maybe_transform, -) -from ._compat import cached_property -from ._version import __version__ -from ._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from ._streaming import Stream as Stream, AsyncStream as AsyncStream -from ._exceptions import APIStatusError, SupermemoryError -from ._base_client import ( - DEFAULT_MAX_RETRIES, - SyncAPIClient, - AsyncAPIClient, - make_request_options, -) -from .types.add_response import AddResponse -from .types.profile_response import ProfileResponse - -if TYPE_CHECKING: - from .resources import search, memories, settings, documents, connections - from .resources.search import SearchResource, AsyncSearchResource - from .resources.memories import MemoriesResource, AsyncMemoriesResource - from .resources.settings import SettingsResource, AsyncSettingsResource - from .resources.documents import DocumentsResource, AsyncDocumentsResource - from .resources.connections import ConnectionsResource, AsyncConnectionsResource - -__all__ = [ - "Timeout", - "Transport", - "ProxiesTypes", - "RequestOptions", - "Supermemory", - "AsyncSupermemory", - "Client", - "AsyncClient", -] - - -class Supermemory(SyncAPIClient): - # client options - api_key: str - - def __init__( - self, - *, - api_key: str | None = None, - base_url: str | httpx.URL | None = None, - timeout: float | Timeout | None | NotGiven = not_given, - max_retries: int = DEFAULT_MAX_RETRIES, - default_headers: Mapping[str, str] | None = None, - default_query: Mapping[str, object] | None = None, - # Configure a custom httpx client. - # We provide a `DefaultHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`. - # See the [httpx documentation](https://www.python-httpx.org/api/#client) for more details. - http_client: httpx.Client | None = None, - # Enable or disable schema validation for data returned by the API. - # When enabled an error APIResponseValidationError is raised - # if the API responds with invalid data for the expected schema. - # - # This parameter may be removed or changed in the future. - # If you rely on this feature, please open a GitHub issue - # outlining your use-case to help us decide if it should be - # part of our public interface in the future. - _strict_response_validation: bool = False, - ) -> None: - """Construct a new synchronous Supermemory client instance. - - This automatically infers the `api_key` argument from the `SUPERMEMORY_API_KEY` environment variable if it is not provided. - """ - if api_key is None: - api_key = os.environ.get("SUPERMEMORY_API_KEY") - if api_key is None: - raise SupermemoryError( - "The api_key client option must be set either by passing api_key to the client or by setting the SUPERMEMORY_API_KEY environment variable" - ) - self.api_key = api_key - - if base_url is None: - base_url = os.environ.get("SUPERMEMORY_BASE_URL") - if base_url is None: - base_url = f"https://api.supermemory.ai" - - custom_headers_env = os.environ.get("SUPERMEMORY_CUSTOM_HEADERS") - if custom_headers_env is not None: - parsed: dict[str, str] = {} - for line in custom_headers_env.split("\n"): - colon = line.find(":") - if colon >= 0: - parsed[line[:colon].strip()] = line[colon + 1 :].strip() - default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})} - - super().__init__( - version=__version__, - base_url=base_url, - max_retries=max_retries, - timeout=timeout, - http_client=http_client, - custom_headers=default_headers, - custom_query=default_query, - _strict_response_validation=_strict_response_validation, - ) - - @cached_property - def memories(self) -> MemoriesResource: - """List, get, update, and delete content and memories""" - from .resources.memories import MemoriesResource - - return MemoriesResource(self) - - @cached_property - def documents(self) -> DocumentsResource: - from .resources.documents import DocumentsResource - - return DocumentsResource(self) - - @cached_property - def search(self) -> SearchResource: - from .resources.search import SearchResource - - return SearchResource(self) - - @cached_property - def settings(self) -> SettingsResource: - """Organization settings""" - from .resources.settings import SettingsResource - - return SettingsResource(self) - - @cached_property - def connections(self) -> ConnectionsResource: - """External service integrations""" - from .resources.connections import ConnectionsResource - - return ConnectionsResource(self) - - @cached_property - def with_raw_response(self) -> SupermemoryWithRawResponse: - return SupermemoryWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> SupermemoryWithStreamedResponse: - return SupermemoryWithStreamedResponse(self) - - @property - @override - def qs(self) -> Querystring: - return Querystring(array_format="comma") - - @property - @override - def auth_headers(self) -> dict[str, str]: - api_key = self.api_key - return {"Authorization": f"Bearer {api_key}"} - - @property - @override - def default_headers(self) -> dict[str, str | Omit]: - return { - **super().default_headers, - "X-Stainless-Async": "false", - **self._custom_headers, - } - - def copy( - self, - *, - api_key: str | None = None, - base_url: str | httpx.URL | None = None, - timeout: float | Timeout | None | NotGiven = not_given, - http_client: httpx.Client | None = None, - max_retries: int | NotGiven = not_given, - default_headers: Mapping[str, str] | None = None, - set_default_headers: Mapping[str, str] | None = None, - default_query: Mapping[str, object] | None = None, - set_default_query: Mapping[str, object] | None = None, - _extra_kwargs: Mapping[str, Any] = {}, - ) -> Self: - """ - Create a new client instance re-using the same options given to the current client with optional overriding. - """ - if default_headers is not None and set_default_headers is not None: - raise ValueError("The `default_headers` and `set_default_headers` arguments are mutually exclusive") - - if default_query is not None and set_default_query is not None: - raise ValueError("The `default_query` and `set_default_query` arguments are mutually exclusive") - - headers = self._custom_headers - if default_headers is not None: - headers = {**headers, **default_headers} - elif set_default_headers is not None: - headers = set_default_headers - - params = self._custom_query - if default_query is not None: - params = {**params, **default_query} - elif set_default_query is not None: - params = set_default_query - - http_client = http_client or self._client - return self.__class__( - api_key=api_key or self.api_key, - base_url=base_url or self.base_url, - timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, - http_client=http_client, - max_retries=max_retries if is_given(max_retries) else self.max_retries, - default_headers=headers, - default_query=params, - **_extra_kwargs, - ) - - # Alias for `copy` for nicer inline usage, e.g. - # client.with_options(timeout=10).foo.create(...) - with_options = copy - - def add( - self, - *, - content: str, - container_tag: str | Omit = omit, - container_tags: SequenceNotStr[str] | Omit = omit, - custom_id: str | Omit = omit, - document_date: str | Omit = omit, - dreaming: Literal["instant", "dynamic"] | Omit = omit, - entity_context: str | Omit = omit, - filepath: str | Omit = omit, - filter_by_metadata: Dict[str, Union[str, float, bool, SequenceNotStr[str]]] | Omit = omit, - metadata: Dict[str, Union[str, float, bool, SequenceNotStr[str]]] | Omit = omit, - task_type: Literal["memory", "superrag"] | 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, - ) -> AddResponse: - """ - Add a document with any content type (text, url, file, etc.) and metadata - - Args: - 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. - - container_tag: Optional tag this document should be containerized by. Max 100 characters, - alphanumeric with hyphens, underscores, and dots only. - - custom_id: Optional custom ID of the document. Max 100 characters, alphanumeric with - hyphens, underscores, and dots only. - - document_date: When this document's content is from, as opposed to when it was uploaded. - Accepts YYYY-MM-DD or a full ISO 8601 timestamp. Memory extraction resolves - relative dates against this instead of the ingestion time, and documents in a - batch are processed oldest-first so newer facts correctly supersede older ones. - Set this whenever you backfill historical content. - - dreaming: Processing mode. "dynamic" (default) groups related documents together so - memories form from coherent, logical units rather than one isolated entry at a - time. "instant" processes each document on its own right away, and bills one - extra operation per document. - - entity_context: Optional entity context for this container tag. Max 1500 characters. Used during - document processing to guide memory extraction. - - filepath: Optional file path for the document. Used by supermemoryfs to store the full - path of the file. - - filter_by_metadata: Optional metadata filter to apply when pulling related memories and profile - during ingestion. Only memories matching these filters will be used as context. - - metadata: Optional metadata for the document. - - task_type: Task type: "memory" (default) for full context layer with SuperRAG built in, - "superrag" for managed RAG as a service. - - 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( - "/v3/documents", - body=maybe_transform( - { - "content": content, - "container_tag": container_tag, - "container_tags": container_tags, - "custom_id": custom_id, - "document_date": document_date, - "dreaming": dreaming, - "entity_context": entity_context, - "filepath": filepath, - "filter_by_metadata": filter_by_metadata, - "metadata": metadata, - "task_type": task_type, - }, - client_add_params.ClientAddParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AddResponse, - ) - - def profile( - self, - *, - container_tag: str, - buckets: SequenceNotStr[str] | Omit = omit, - filters: client_profile_params.Filters | Omit = omit, - include: List[Literal["static", "dynamic", "buckets"]] | Omit = omit, - q: str | Omit = omit, - threshold: float | 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, - ) -> ProfileResponse: - """ - 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. - - buckets: Specific bucket keys to return. Omit to return all configured buckets. Only - relevant when "buckets" is included. - - filters: Optional metadata filters to apply to profile results and search results. - Supports complex AND/OR queries with multiple conditions. - - include: Profile sections to return. Omit to return all sections. Pass a subset to reduce - payload — e.g. ["buckets"] skips static and dynamic entirely. - - q: Optional search query to include search results in the response - - threshold: Threshold for search results. Only results with a score above this threshold - will be included. - - 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, - "buckets": buckets, - "filters": filters, - "include": include, - "q": q, - "threshold": threshold, - }, - client_profile_params.ClientProfileParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ProfileResponse, - ) - - @override - def _make_status_error( - self, - err_msg: str, - *, - body: object, - response: httpx.Response, - ) -> APIStatusError: - if response.status_code == 400: - return _exceptions.BadRequestError(err_msg, response=response, body=body) - - if response.status_code == 401: - return _exceptions.AuthenticationError(err_msg, response=response, body=body) - - if response.status_code == 403: - return _exceptions.PermissionDeniedError(err_msg, response=response, body=body) - - if response.status_code == 404: - return _exceptions.NotFoundError(err_msg, response=response, body=body) - - if response.status_code == 409: - return _exceptions.ConflictError(err_msg, response=response, body=body) - - if response.status_code == 422: - return _exceptions.UnprocessableEntityError(err_msg, response=response, body=body) - - if response.status_code == 429: - return _exceptions.RateLimitError(err_msg, response=response, body=body) - - if response.status_code >= 500: - return _exceptions.InternalServerError(err_msg, response=response, body=body) - return APIStatusError(err_msg, response=response, body=body) - - -class AsyncSupermemory(AsyncAPIClient): - # client options - api_key: str - - def __init__( - self, - *, - api_key: str | None = None, - base_url: str | httpx.URL | None = None, - timeout: float | Timeout | None | NotGiven = not_given, - max_retries: int = DEFAULT_MAX_RETRIES, - default_headers: Mapping[str, str] | None = None, - default_query: Mapping[str, object] | None = None, - # Configure a custom httpx client. - # We provide a `DefaultAsyncHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`. - # See the [httpx documentation](https://www.python-httpx.org/api/#asyncclient) for more details. - http_client: httpx.AsyncClient | None = None, - # Enable or disable schema validation for data returned by the API. - # When enabled an error APIResponseValidationError is raised - # if the API responds with invalid data for the expected schema. - # - # This parameter may be removed or changed in the future. - # If you rely on this feature, please open a GitHub issue - # outlining your use-case to help us decide if it should be - # part of our public interface in the future. - _strict_response_validation: bool = False, - ) -> None: - """Construct a new async AsyncSupermemory client instance. - - This automatically infers the `api_key` argument from the `SUPERMEMORY_API_KEY` environment variable if it is not provided. - """ - if api_key is None: - api_key = os.environ.get("SUPERMEMORY_API_KEY") - if api_key is None: - raise SupermemoryError( - "The api_key client option must be set either by passing api_key to the client or by setting the SUPERMEMORY_API_KEY environment variable" - ) - self.api_key = api_key - - if base_url is None: - base_url = os.environ.get("SUPERMEMORY_BASE_URL") - if base_url is None: - base_url = f"https://api.supermemory.ai" - - custom_headers_env = os.environ.get("SUPERMEMORY_CUSTOM_HEADERS") - if custom_headers_env is not None: - parsed: dict[str, str] = {} - for line in custom_headers_env.split("\n"): - colon = line.find(":") - if colon >= 0: - parsed[line[:colon].strip()] = line[colon + 1 :].strip() - default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})} - - super().__init__( - version=__version__, - base_url=base_url, - max_retries=max_retries, - timeout=timeout, - http_client=http_client, - custom_headers=default_headers, - custom_query=default_query, - _strict_response_validation=_strict_response_validation, - ) - - @cached_property - def memories(self) -> AsyncMemoriesResource: - """List, get, update, and delete content and memories""" - from .resources.memories import AsyncMemoriesResource - - return AsyncMemoriesResource(self) - - @cached_property - def documents(self) -> AsyncDocumentsResource: - from .resources.documents import AsyncDocumentsResource - - return AsyncDocumentsResource(self) - - @cached_property - def search(self) -> AsyncSearchResource: - from .resources.search import AsyncSearchResource - - return AsyncSearchResource(self) - - @cached_property - def settings(self) -> AsyncSettingsResource: - """Organization settings""" - from .resources.settings import AsyncSettingsResource - - return AsyncSettingsResource(self) - - @cached_property - def connections(self) -> AsyncConnectionsResource: - """External service integrations""" - from .resources.connections import AsyncConnectionsResource - - return AsyncConnectionsResource(self) - - @cached_property - def with_raw_response(self) -> AsyncSupermemoryWithRawResponse: - return AsyncSupermemoryWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncSupermemoryWithStreamedResponse: - return AsyncSupermemoryWithStreamedResponse(self) - - @property - @override - def qs(self) -> Querystring: - return Querystring(array_format="comma") - - @property - @override - def auth_headers(self) -> dict[str, str]: - api_key = self.api_key - return {"Authorization": f"Bearer {api_key}"} - - @property - @override - def default_headers(self) -> dict[str, str | Omit]: - return { - **super().default_headers, - "X-Stainless-Async": f"async:{get_async_library()}", - **self._custom_headers, - } - - def copy( - self, - *, - api_key: str | None = None, - base_url: str | httpx.URL | None = None, - timeout: float | Timeout | None | NotGiven = not_given, - http_client: httpx.AsyncClient | None = None, - max_retries: int | NotGiven = not_given, - default_headers: Mapping[str, str] | None = None, - set_default_headers: Mapping[str, str] | None = None, - default_query: Mapping[str, object] | None = None, - set_default_query: Mapping[str, object] | None = None, - _extra_kwargs: Mapping[str, Any] = {}, - ) -> Self: - """ - Create a new client instance re-using the same options given to the current client with optional overriding. - """ - if default_headers is not None and set_default_headers is not None: - raise ValueError("The `default_headers` and `set_default_headers` arguments are mutually exclusive") - - if default_query is not None and set_default_query is not None: - raise ValueError("The `default_query` and `set_default_query` arguments are mutually exclusive") - - headers = self._custom_headers - if default_headers is not None: - headers = {**headers, **default_headers} - elif set_default_headers is not None: - headers = set_default_headers - - params = self._custom_query - if default_query is not None: - params = {**params, **default_query} - elif set_default_query is not None: - params = set_default_query - - http_client = http_client or self._client - return self.__class__( - api_key=api_key or self.api_key, - base_url=base_url or self.base_url, - timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, - http_client=http_client, - max_retries=max_retries if is_given(max_retries) else self.max_retries, - default_headers=headers, - default_query=params, - **_extra_kwargs, - ) - - # Alias for `copy` for nicer inline usage, e.g. - # client.with_options(timeout=10).foo.create(...) - with_options = copy - - async def add( - self, - *, - content: str, - container_tag: str | Omit = omit, - container_tags: SequenceNotStr[str] | Omit = omit, - custom_id: str | Omit = omit, - document_date: str | Omit = omit, - dreaming: Literal["instant", "dynamic"] | Omit = omit, - entity_context: str | Omit = omit, - filepath: str | Omit = omit, - filter_by_metadata: Dict[str, Union[str, float, bool, SequenceNotStr[str]]] | Omit = omit, - metadata: Dict[str, Union[str, float, bool, SequenceNotStr[str]]] | Omit = omit, - task_type: Literal["memory", "superrag"] | 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, - ) -> AddResponse: - """ - Add a document with any content type (text, url, file, etc.) and metadata - - Args: - 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. - - container_tag: Optional tag this document should be containerized by. Max 100 characters, - alphanumeric with hyphens, underscores, and dots only. - - custom_id: Optional custom ID of the document. Max 100 characters, alphanumeric with - hyphens, underscores, and dots only. - - document_date: When this document's content is from, as opposed to when it was uploaded. - Accepts YYYY-MM-DD or a full ISO 8601 timestamp. Memory extraction resolves - relative dates against this instead of the ingestion time, and documents in a - batch are processed oldest-first so newer facts correctly supersede older ones. - Set this whenever you backfill historical content. - - dreaming: Processing mode. "dynamic" (default) groups related documents together so - memories form from coherent, logical units rather than one isolated entry at a - time. "instant" processes each document on its own right away, and bills one - extra operation per document. - - entity_context: Optional entity context for this container tag. Max 1500 characters. Used during - document processing to guide memory extraction. - - filepath: Optional file path for the document. Used by supermemoryfs to store the full - path of the file. - - filter_by_metadata: Optional metadata filter to apply when pulling related memories and profile - during ingestion. Only memories matching these filters will be used as context. - - metadata: Optional metadata for the document. - - task_type: Task type: "memory" (default) for full context layer with SuperRAG built in, - "superrag" for managed RAG as a service. - - 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( - "/v3/documents", - body=await async_maybe_transform( - { - "content": content, - "container_tag": container_tag, - "container_tags": container_tags, - "custom_id": custom_id, - "document_date": document_date, - "dreaming": dreaming, - "entity_context": entity_context, - "filepath": filepath, - "filter_by_metadata": filter_by_metadata, - "metadata": metadata, - "task_type": task_type, - }, - client_add_params.ClientAddParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=AddResponse, - ) - - async def profile( - self, - *, - container_tag: str, - buckets: SequenceNotStr[str] | Omit = omit, - filters: client_profile_params.Filters | Omit = omit, - include: List[Literal["static", "dynamic", "buckets"]] | Omit = omit, - q: str | Omit = omit, - threshold: float | 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, - ) -> ProfileResponse: - """ - 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. - - buckets: Specific bucket keys to return. Omit to return all configured buckets. Only - relevant when "buckets" is included. - - filters: Optional metadata filters to apply to profile results and search results. - Supports complex AND/OR queries with multiple conditions. - - include: Profile sections to return. Omit to return all sections. Pass a subset to reduce - payload — e.g. ["buckets"] skips static and dynamic entirely. - - q: Optional search query to include search results in the response - - threshold: Threshold for search results. Only results with a score above this threshold - will be included. - - 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, - "buckets": buckets, - "filters": filters, - "include": include, - "q": q, - "threshold": threshold, - }, - client_profile_params.ClientProfileParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ProfileResponse, - ) - - @override - def _make_status_error( - self, - err_msg: str, - *, - body: object, - response: httpx.Response, - ) -> APIStatusError: - if response.status_code == 400: - return _exceptions.BadRequestError(err_msg, response=response, body=body) - - if response.status_code == 401: - return _exceptions.AuthenticationError(err_msg, response=response, body=body) - - if response.status_code == 403: - return _exceptions.PermissionDeniedError(err_msg, response=response, body=body) - - if response.status_code == 404: - return _exceptions.NotFoundError(err_msg, response=response, body=body) - - if response.status_code == 409: - return _exceptions.ConflictError(err_msg, response=response, body=body) - - if response.status_code == 422: - return _exceptions.UnprocessableEntityError(err_msg, response=response, body=body) - - if response.status_code == 429: - return _exceptions.RateLimitError(err_msg, response=response, body=body) - - if response.status_code >= 500: - return _exceptions.InternalServerError(err_msg, response=response, body=body) - return APIStatusError(err_msg, response=response, body=body) - - -class SupermemoryWithRawResponse: - _client: Supermemory - - def __init__(self, client: Supermemory) -> None: - self._client = client - - self.add = to_raw_response_wrapper( - client.add, - ) - self.profile = to_raw_response_wrapper( - client.profile, - ) - - @cached_property - def memories(self) -> memories.MemoriesResourceWithRawResponse: - """List, get, update, and delete content and memories""" - from .resources.memories import MemoriesResourceWithRawResponse - - return MemoriesResourceWithRawResponse(self._client.memories) - - @cached_property - def documents(self) -> documents.DocumentsResourceWithRawResponse: - from .resources.documents import DocumentsResourceWithRawResponse - - return DocumentsResourceWithRawResponse(self._client.documents) - - @cached_property - def search(self) -> search.SearchResourceWithRawResponse: - from .resources.search import SearchResourceWithRawResponse - - return SearchResourceWithRawResponse(self._client.search) - - @cached_property - def settings(self) -> settings.SettingsResourceWithRawResponse: - """Organization settings""" - from .resources.settings import SettingsResourceWithRawResponse - - return SettingsResourceWithRawResponse(self._client.settings) - - @cached_property - def connections(self) -> connections.ConnectionsResourceWithRawResponse: - """External service integrations""" - from .resources.connections import ConnectionsResourceWithRawResponse - - return ConnectionsResourceWithRawResponse(self._client.connections) - - -class AsyncSupermemoryWithRawResponse: - _client: AsyncSupermemory - - def __init__(self, client: AsyncSupermemory) -> None: - self._client = client - - self.add = async_to_raw_response_wrapper( - client.add, - ) - self.profile = async_to_raw_response_wrapper( - client.profile, - ) - - @cached_property - def memories(self) -> memories.AsyncMemoriesResourceWithRawResponse: - """List, get, update, and delete content and memories""" - from .resources.memories import AsyncMemoriesResourceWithRawResponse - - return AsyncMemoriesResourceWithRawResponse(self._client.memories) - - @cached_property - def documents(self) -> documents.AsyncDocumentsResourceWithRawResponse: - from .resources.documents import AsyncDocumentsResourceWithRawResponse - - return AsyncDocumentsResourceWithRawResponse(self._client.documents) - - @cached_property - def search(self) -> search.AsyncSearchResourceWithRawResponse: - from .resources.search import AsyncSearchResourceWithRawResponse - - return AsyncSearchResourceWithRawResponse(self._client.search) - - @cached_property - def settings(self) -> settings.AsyncSettingsResourceWithRawResponse: - """Organization settings""" - from .resources.settings import AsyncSettingsResourceWithRawResponse - - return AsyncSettingsResourceWithRawResponse(self._client.settings) - - @cached_property - def connections(self) -> connections.AsyncConnectionsResourceWithRawResponse: - """External service integrations""" - from .resources.connections import AsyncConnectionsResourceWithRawResponse - - return AsyncConnectionsResourceWithRawResponse(self._client.connections) - - -class SupermemoryWithStreamedResponse: - _client: Supermemory - - def __init__(self, client: Supermemory) -> None: - self._client = client - - self.add = to_streamed_response_wrapper( - client.add, - ) - self.profile = to_streamed_response_wrapper( - client.profile, - ) - - @cached_property - def memories(self) -> memories.MemoriesResourceWithStreamingResponse: - """List, get, update, and delete content and memories""" - from .resources.memories import MemoriesResourceWithStreamingResponse - - return MemoriesResourceWithStreamingResponse(self._client.memories) - - @cached_property - def documents(self) -> documents.DocumentsResourceWithStreamingResponse: - from .resources.documents import DocumentsResourceWithStreamingResponse - - return DocumentsResourceWithStreamingResponse(self._client.documents) - - @cached_property - def search(self) -> search.SearchResourceWithStreamingResponse: - from .resources.search import SearchResourceWithStreamingResponse - - return SearchResourceWithStreamingResponse(self._client.search) - - @cached_property - def settings(self) -> settings.SettingsResourceWithStreamingResponse: - """Organization settings""" - from .resources.settings import SettingsResourceWithStreamingResponse - - return SettingsResourceWithStreamingResponse(self._client.settings) - - @cached_property - def connections(self) -> connections.ConnectionsResourceWithStreamingResponse: - """External service integrations""" - from .resources.connections import ConnectionsResourceWithStreamingResponse - - return ConnectionsResourceWithStreamingResponse(self._client.connections) - - -class AsyncSupermemoryWithStreamedResponse: - _client: AsyncSupermemory - - def __init__(self, client: AsyncSupermemory) -> None: - self._client = client - - self.add = async_to_streamed_response_wrapper( - client.add, - ) - self.profile = async_to_streamed_response_wrapper( - client.profile, - ) - - @cached_property - def memories(self) -> memories.AsyncMemoriesResourceWithStreamingResponse: - """List, get, update, and delete content and memories""" - from .resources.memories import AsyncMemoriesResourceWithStreamingResponse - - return AsyncMemoriesResourceWithStreamingResponse(self._client.memories) - - @cached_property - def documents(self) -> documents.AsyncDocumentsResourceWithStreamingResponse: - from .resources.documents import AsyncDocumentsResourceWithStreamingResponse - - return AsyncDocumentsResourceWithStreamingResponse(self._client.documents) - - @cached_property - def search(self) -> search.AsyncSearchResourceWithStreamingResponse: - from .resources.search import AsyncSearchResourceWithStreamingResponse - - return AsyncSearchResourceWithStreamingResponse(self._client.search) - - @cached_property - def settings(self) -> settings.AsyncSettingsResourceWithStreamingResponse: - """Organization settings""" - from .resources.settings import AsyncSettingsResourceWithStreamingResponse - - return AsyncSettingsResourceWithStreamingResponse(self._client.settings) - - @cached_property - def connections(self) -> connections.AsyncConnectionsResourceWithStreamingResponse: - """External service integrations""" - from .resources.connections import AsyncConnectionsResourceWithStreamingResponse - - return AsyncConnectionsResourceWithStreamingResponse(self._client.connections) - - -Client = Supermemory - -AsyncClient = AsyncSupermemory diff --git a/src/supermemory/_compat.py b/src/supermemory/_compat.py index e6690a4f..a9921870 100644 --- a/src/supermemory/_compat.py +++ b/src/supermemory/_compat.py @@ -1,226 +1,541 @@ +"""3.x client conventions on top of the Fern-generated v5 client. + +Hand-written — lives in custom/ and is copied into src/supermemory by +scripts/generate. It never names an endpoint: methods are wrapped generically, +so spec changes regenerate cleanly without touching this file. + +Kept from the 3.x (Stainless) SDK: +- constructor options: api_key, base_url, timeout, max_retries, default_headers, + default_query, http_client; SUPERMEMORY_API_KEY / SUPERMEMORY_BASE_URL / + SUPERMEMORY_CUSTOM_HEADERS +- per-call options: extra_headers, extra_query, extra_body, timeout +- client.with_options(...) / client.copy(...) +- the exception hierarchy (SupermemoryError → APIError → APIStatusError → + NotFoundError, RateLimitError, …; APIConnectionError, APITimeoutError) with + .status_code, .body, .message, .request, .response +- with_raw_response → .parse(), .headers, .status_code, .http_response, and + model.to_dict() / model.to_json() (added to generated core by custom/patches/) +""" + from __future__ import annotations -from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload -from datetime import date, datetime -from typing_extensions import Self, Literal, TypedDict +import contextvars +import functools +import inspect +import os +from collections.abc import Callable, Mapping +from typing import Any, Literal, cast +import httpx import pydantic -from pydantic.fields import FieldInfo +from typing_extensions import Self -from ._types import IncEx, StrBytesIntFloat +from .client import AsyncSupermemory as _GeneratedAsyncSupermemory +from .client import Supermemory as _GeneratedSupermemory +from .core.api_error import ApiError +from .core.pydantic_utilities import IS_PYDANTIC_V2 -_T = TypeVar("_T") -_ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel) +__all__ = [ + "DEFAULT_MAX_RETRIES", + "DEFAULT_TIMEOUT", + "APIConnectionError", + "APIError", + "APIResponseValidationError", + "APIStatusError", + "APITimeoutError", + "AsyncClient", + "AsyncSupermemory", + "AuthenticationError", + "BadRequestError", + "Client", + "ConflictError", + "InternalServerError", + "NotFoundError", + "PermissionDeniedError", + "RateLimitError", + "Supermemory", + "SupermemoryError", + "Timeout", + "UnprocessableEntityError", +] -# --------------- Pydantic v2, v3 compatibility --------------- +Timeout = httpx.Timeout +DEFAULT_TIMEOUT = httpx.Timeout(timeout=60, connect=5.0) +DEFAULT_MAX_RETRIES = 2 -# Pyright incorrectly reports some of our functions as overriding a method when they don't -# pyright: reportIncompatibleMethodOverride=false +# ----------------------------------------------------------------- exceptions -PYDANTIC_V1 = pydantic.VERSION.startswith("1.") -if TYPE_CHECKING: +class SupermemoryError(Exception): + pass - def parse_date(value: date | StrBytesIntFloat) -> date: # noqa: ARG001 - ... - def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: # noqa: ARG001 - ... +class APIError(SupermemoryError): + message: str + request: httpx.Request | None + body: object - def get_args(t: type[Any]) -> tuple[Any, ...]: # noqa: ARG001 - ... + def __init__(self, message: str, request: httpx.Request | None, *, body: object) -> None: + # Not super(): raised errors also inherit the generated ApiError, whose + # keyword-only __init__ would be next in the MRO. + Exception.__init__(self, message) + self.request = request + self.message = message + self.body = body - def is_union(tp: type[Any] | None) -> bool: # noqa: ARG001 - ... - def get_origin(t: type[Any]) -> type[Any] | None: # noqa: ARG001 - ... +class APIResponseValidationError(APIError): + response: httpx.Response + status_code: int - def is_literal_type(type_: type[Any]) -> bool: # noqa: ARG001 - ... + def __init__(self, response: httpx.Response, body: object, *, message: str | None = None) -> None: + super().__init__(message or "Data returned by API invalid for expected schema.", response.request, body=body) + self.response = response + self.status_code = response.status_code - def is_typeddict(type_: type[Any]) -> bool: # noqa: ARG001 - ... -else: - # v1 re-exports - if PYDANTIC_V1: - from pydantic.typing import ( - get_args as get_args, - is_union as is_union, - get_origin as get_origin, - is_typeddict as is_typeddict, - is_literal_type as is_literal_type, - ) - from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime - else: - from ._utils import ( - get_args as get_args, - is_union as is_union, - get_origin as get_origin, - parse_date as parse_date, - is_typeddict as is_typeddict, - parse_datetime as parse_datetime, - is_literal_type as is_literal_type, - ) +class APIStatusError(APIError): + """Raised when an API response has a status code of 4xx or 5xx.""" + response: httpx.Response | None + status_code: int + headers: dict[str, str] | None -# refactored config -if TYPE_CHECKING: - from pydantic import ConfigDict as ConfigDict -else: - if PYDANTIC_V1: - # TODO: provide an error message here? - ConfigDict = None - else: - from pydantic import ConfigDict as ConfigDict + def __init__(self, message: str, *, response: httpx.Response | None, body: object, status_code: int) -> None: + super().__init__(message, response.request if response is not None else None, body=body) + self.response = response + self.status_code = status_code + self.headers = dict(response.headers) if response is not None else None -# renamed methods / properties -def parse_obj(model: type[_ModelT], value: object) -> _ModelT: - if PYDANTIC_V1: - return cast(_ModelT, model.parse_obj(value)) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] - else: - return model.model_validate(value) +class APIConnectionError(APIError): + def __init__(self, *, message: str = "Connection error.", request: httpx.Request | None) -> None: + super().__init__(message, request, body=None) -def field_is_required(field: FieldInfo) -> bool: - if PYDANTIC_V1: - return field.required # type: ignore - return field.is_required() +class APITimeoutError(APIConnectionError): + def __init__(self, request: httpx.Request | None) -> None: + super().__init__(message="Request timed out.", request=request) -def field_get_default(field: FieldInfo) -> Any: - value = field.get_default() - if PYDANTIC_V1: - return value - from pydantic_core import PydanticUndefined +class BadRequestError(APIStatusError): + status_code: Literal[400] = 400 # type: ignore[assignment] - if value == PydanticUndefined: - return None - return value +class AuthenticationError(APIStatusError): + status_code: Literal[401] = 401 # type: ignore[assignment] -def field_outer_type(field: FieldInfo) -> Any: - if PYDANTIC_V1: - return field.outer_type_ # type: ignore - return field.annotation +class PermissionDeniedError(APIStatusError): + status_code: Literal[403] = 403 # type: ignore[assignment] -def get_model_config(model: type[pydantic.BaseModel]) -> Any: - if PYDANTIC_V1: - return model.__config__ # type: ignore - return model.model_config +class NotFoundError(APIStatusError): + status_code: Literal[404] = 404 # type: ignore[assignment] -def get_model_fields(model: type[pydantic.BaseModel]) -> dict[str, FieldInfo]: - if PYDANTIC_V1: - return model.__fields__ # type: ignore - return model.model_fields +class ConflictError(APIStatusError): + status_code: Literal[409] = 409 # type: ignore[assignment] -def model_copy(model: _ModelT, *, deep: bool = False) -> _ModelT: - if PYDANTIC_V1: - return model.copy(deep=deep) # type: ignore - return model.model_copy(deep=deep) +class UnprocessableEntityError(APIStatusError): + status_code: Literal[422] = 422 # type: ignore[assignment] -def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: - if PYDANTIC_V1: - return model.json(indent=indent) # type: ignore - return model.model_dump_json(indent=indent) +class RateLimitError(APIStatusError): + status_code: Literal[429] = 429 # type: ignore[assignment] -class _ModelDumpKwargs(TypedDict, total=False): - by_alias: bool +class InternalServerError(APIStatusError): + pass -def model_dump( - model: pydantic.BaseModel, - *, - exclude: IncEx | None = None, - exclude_unset: bool = False, - exclude_defaults: bool = False, - warnings: bool = True, - mode: Literal["json", "python"] = "python", - by_alias: bool | None = None, -) -> dict[str, Any]: - if (not PYDANTIC_V1) or hasattr(model, "model_dump"): - kwargs: _ModelDumpKwargs = {} - if by_alias is not None: - kwargs["by_alias"] = by_alias - return model.model_dump( - mode=mode, - exclude=exclude, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - # warnings are not supported in Pydantic v1 - warnings=True if PYDANTIC_V1 else warnings, - **kwargs, + +_STATUS_ERRORS: dict[int, type[APIStatusError]] = { + 400: BadRequestError, + 401: AuthenticationError, + 403: PermissionDeniedError, + 404: NotFoundError, + 409: ConflictError, + 422: UnprocessableEntityError, + 429: RateLimitError, +} + +# The httpx.Response behind the most recent request in this thread / task, so +# status errors can carry .response and .request like 3.x did. +_last_response: contextvars.ContextVar[httpx.Response | None] = contextvars.ContextVar( + "supermemory_last_response", default=None +) + + +def _capture_response(response: httpx.Response) -> None: + _last_response.set(response) + + +async def _acapture_response(response: httpx.Response) -> None: + _last_response.set(response) + + +_combined_error_classes: dict[tuple[type[ApiError], type[APIStatusError]], type[APIStatusError]] = {} + + +def _combined_error_class(generated: type[ApiError], compat: type[APIStatusError]) -> type[APIStatusError]: + # Subclass of both, so `except supermemory.NotFoundError` (3.x) and + # `except supermemory.errors.NotFoundError` (generated) both catch it. + key = (generated, compat) + if key not in _combined_error_classes: + _combined_error_classes[key] = cast( + type[APIStatusError], + type(compat.__name__, (compat, generated), {"__module__": "supermemory", "__str__": Exception.__str__}), ) - return cast( - "dict[str, Any]", - model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] - exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias) - ), - ) + return _combined_error_classes[key] + + +def _status_error(err: ApiError) -> APIStatusError: + status = err.status_code or 0 + compat = _STATUS_ERRORS.get(status) or (InternalServerError if status >= 500 else APIStatusError) + response = _last_response.get() + if response is not None and response.status_code != status: + response = None + body = err.body + if response is not None: + # The decoded JSON (or text) the API sent, not a re-serialized model. + try: + body = response.json() + except ValueError: + body = response.text + elif isinstance(body, pydantic.BaseModel): + body = ( + body.model_dump(by_alias=True, exclude_unset=True) + if IS_PYDANTIC_V2 + else body.dict(by_alias=True, exclude_unset=True) + ) + cls = _combined_error_class(type(err), compat) + new = cls.__new__(cls) + APIStatusError.__init__(new, f"Error code: {status} - {body}", response=response, body=body, status_code=status) + if response is None: + new.headers = err.headers + return new + + +def _request_of(err: httpx.HTTPError) -> httpx.Request | None: + try: + return err.request + except RuntimeError: + return None -def model_parse(model: type[_ModelT], data: Any) -> _ModelT: - if PYDANTIC_V1: - return model.parse_obj(data) # pyright: ignore[reportDeprecated] - return model.model_validate(data) +# ---------------------------------------------------------- per-call options + + +class _Defaults: + def __init__(self, default_query: Mapping[str, object] | None) -> None: + self.default_query = dict(default_query or {}) + + +def _apply_per_call_options(kwargs: dict[str, Any], defaults: _Defaults) -> None: + # scripts/apply_custom.py adds these parameters to every generated method + # signature (for type checkers); here they become Fern request_options. + extra_headers = kwargs.pop("extra_headers", None) + extra_query = kwargs.pop("extra_query", None) + extra_body = kwargs.pop("extra_body", None) + timeout = kwargs.pop("timeout", None) + query = {**defaults.default_query, **(extra_query or {})} + if not (extra_headers or query or extra_body or timeout is not None): + return + request_options: dict[str, Any] = dict(kwargs.get("request_options") or {}) + if extra_headers: + request_options["additional_headers"] = {**extra_headers, **request_options.get("additional_headers", {})} + if query: + request_options["additional_query_parameters"] = { + **query, + **request_options.get("additional_query_parameters", {}), + } + if extra_body: + request_options["additional_body_parameters"] = { + **extra_body, + **request_options.get("additional_body_parameters", {}), + } + if timeout is not None: + request_options["timeout"] = timeout.read if isinstance(timeout, httpx.Timeout) else timeout + kwargs["request_options"] = request_options + + +def _wrap_method(fn: Callable[..., Any], defaults: _Defaults) -> Callable[..., Any]: + if inspect.iscoroutinefunction(fn): + + @functools.wraps(fn) + async def async_wrapper(*args: Any, **kwargs: Any) -> Any: + _apply_per_call_options(kwargs, defaults) + try: + return await fn(*args, **kwargs) + except ApiError as err: + raise _status_error(err).with_traceback(err.__traceback__) from None + except httpx.TimeoutException as err: + raise APITimeoutError(request=_request_of(err)) from err + except httpx.TransportError as err: + raise APIConnectionError(request=_request_of(err)) from err + + return async_wrapper + + @functools.wraps(fn) + def wrapper(*args: Any, **kwargs: Any) -> Any: + _apply_per_call_options(kwargs, defaults) + try: + return fn(*args, **kwargs) + except ApiError as err: + raise _status_error(err).with_traceback(err.__traceback__) from None + except httpx.TimeoutException as err: + raise APITimeoutError(request=_request_of(err)) from err + except httpx.TransportError as err: + raise APIConnectionError(request=_request_of(err)) from err + + return wrapper + + +def _is_generated_client(value: object) -> bool: + cls = type(value) + return cls.__module__.startswith(f"{__package__}.") and cls.__name__.endswith(("Client", "Supermemory")) + + +def _wrap(name: str, value: Any, defaults: _Defaults) -> Any: + if name.startswith("_"): + return value + if inspect.ismethod(value) and "request_options" in inspect.signature(value).parameters: + return _wrap_method(value, defaults) + if _is_generated_client(value): + return _Resource(value, defaults) + return value -# generic models -if TYPE_CHECKING: +class _Resource: + """Wraps a generated sub-client (client.documents, client.with_raw_response, …).""" - class GenericModel(pydantic.BaseModel): ... + def __init__(self, target: Any, defaults: _Defaults) -> None: + self._target = target + self._defaults = defaults -else: - if PYDANTIC_V1: - import pydantic.generics + def __getattr__(self, name: str) -> Any: + return _wrap(name, getattr(self._target, name), self._defaults) - class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ... - else: - # there no longer needs to be a distinction in v2 but - # we still have to create our own subclass to avoid - # inconsistent MRO ordering errors - class GenericModel(pydantic.BaseModel): ... + def __dir__(self) -> Any: + return dir(self._target) + def __repr__(self) -> str: + return repr(self._target) -# cached properties -if TYPE_CHECKING: - cached_property = property - # we define a separate type (copied from typeshed) - # that represents that `cached_property` is `set`able - # at runtime, which differs from `@property`. - # - # this is a separate type as editors likely special case - # `@property` and we don't want to cause issues just to have - # more helpful internal types. +# -------------------------------------------------------------------- clients - class typed_cached_property(Generic[_T]): - func: Callable[[Any], _T] - attrname: str | None - def __init__(self, func: Callable[[Any], _T]) -> None: ... +def _env_headers(default_headers: Mapping[str, str] | None) -> dict[str, str]: + headers: dict[str, str] = {} + for line in os.environ.get("SUPERMEMORY_CUSTOM_HEADERS", "").split("\n"): + colon = line.find(":") + if colon >= 0: + headers[line[:colon].strip()] = line[colon + 1 :].strip() + return {**headers, **(default_headers or {})} - @overload - def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: ... - @overload - def __get__(self, instance: object, owner: type[Any] | None = None) -> _T: ... +def _resolve(api_key: str | None, base_url: str | httpx.URL | None) -> tuple[str, str]: + api_key = api_key if api_key is not None else os.environ.get("SUPERMEMORY_API_KEY") + if api_key is None: + raise SupermemoryError( + "The api_key client option must be set either by passing api_key to the client " + "or by setting the SUPERMEMORY_API_KEY environment variable" + ) + base_url = base_url if base_url is not None else os.environ.get("SUPERMEMORY_BASE_URL") + return api_key, str(base_url or "https://api.supermemory.ai").rstrip("/") + - def __get__(self, instance: object, owner: type[Any] | None = None) -> _T | Self: - raise NotImplementedError() +def _resolve_timeout(timeout: float | httpx.Timeout | None) -> httpx.Timeout: + return ( + timeout + if isinstance(timeout, httpx.Timeout) + else httpx.Timeout(timeout) + if timeout is not None + else DEFAULT_TIMEOUT + ) - def __set_name__(self, owner: type[Any], name: str) -> None: ... - # __set__ is not defined at runtime, but @cached_property is designed to be settable - def __set__(self, instance: object, value: _T) -> None: ... -else: - from functools import cached_property as cached_property +def _add_hook(client: httpx.Client | httpx.AsyncClient, hook: Callable[..., Any]) -> None: + hooks = client.event_hooks + if hook not in hooks["response"]: + hooks["response"] = [*hooks["response"], hook] + client.event_hooks = hooks + + +_OWN_ATTRIBUTES = frozenset({"with_options", "copy", "api_key", "base_url", "timeout", "max_retries", "close"}) + + +class Supermemory(_GeneratedSupermemory): + """Synchronous Supermemory client (3.x-compatible options; v5 API methods).""" + + def __init__( + self, + *, + api_key: str | None = None, + base_url: str | httpx.URL | None = None, + timeout: float | httpx.Timeout | None = None, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + http_client: httpx.Client | None = None, + **kwargs: Any, + ) -> None: + self.api_key, self.base_url = _resolve(api_key, base_url) + self.timeout = _resolve_timeout(timeout) + self.max_retries = max_retries + headers = _env_headers({**(default_headers or {}), **kwargs.pop("headers", {})}) + http_client = http_client or kwargs.pop("httpx_client", None) + self._options: dict[str, Any] = dict( + api_key=self.api_key, + base_url=self.base_url, + timeout=self.timeout, + max_retries=max_retries, + default_headers=headers, + default_query=dict(default_query or {}), + http_client=http_client, + **kwargs, + ) + self._owned_http_client = http_client is None + self._http_client = http_client or httpx.Client(timeout=self.timeout, follow_redirects=True) + _add_hook(self._http_client, _capture_response) + self._defaults = _Defaults(default_query) + super().__init__( + api_key=self.api_key, + base_url=self.base_url, + timeout=self.timeout.read, + max_retries=max_retries, + headers=headers, + httpx_client=self._http_client, + **kwargs, + ) + + def __getattribute__(self, name: str) -> Any: + value = super().__getattribute__(name) + if name.startswith("_") or name in _OWN_ATTRIBUTES: + return value + return _wrap(name, value, super().__getattribute__("_defaults")) + + def copy( + self, + *, + set_default_headers: Mapping[str, str] | None = None, + set_default_query: Mapping[str, object] | None = None, + **overrides: Any, + ) -> Supermemory: + """A new client with some options changed; the rest, including the connection pool, are kept.""" + options = dict(self._options) + if self._owned_http_client: + options["http_client"] = self._http_client + options["default_headers"] = set_default_headers or { + **options["default_headers"], + **(overrides.pop("default_headers", None) or {}), + } + options["default_query"] = set_default_query or { + **options["default_query"], + **(overrides.pop("default_query", None) or {}), + } + options.update(overrides) + return type(self)(**options) + + with_options = copy + + def close(self) -> None: + if self._owned_http_client: + self._http_client.close() + + def __enter__(self) -> Self: + return self + + def __exit__(self, *_: object) -> None: + self.close() + + +class AsyncSupermemory(_GeneratedAsyncSupermemory): + """Asynchronous Supermemory client (3.x-compatible options; v5 API methods).""" + + def __init__( + self, + *, + api_key: str | None = None, + base_url: str | httpx.URL | None = None, + timeout: float | httpx.Timeout | None = None, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + http_client: httpx.AsyncClient | None = None, + **kwargs: Any, + ) -> None: + self.api_key, self.base_url = _resolve(api_key, base_url) + self.timeout = _resolve_timeout(timeout) + self.max_retries = max_retries + headers = _env_headers({**(default_headers or {}), **kwargs.pop("headers", {})}) + http_client = http_client or kwargs.pop("httpx_client", None) + self._options: dict[str, Any] = dict( + api_key=self.api_key, + base_url=self.base_url, + timeout=self.timeout, + max_retries=max_retries, + default_headers=headers, + default_query=dict(default_query or {}), + http_client=http_client, + **kwargs, + ) + self._owned_http_client = http_client is None + self._http_client = http_client or httpx.AsyncClient(timeout=self.timeout, follow_redirects=True) + _add_hook(self._http_client, _acapture_response) + self._defaults = _Defaults(default_query) + super().__init__( + api_key=self.api_key, + base_url=self.base_url, + timeout=self.timeout.read, + max_retries=max_retries, + headers=headers, + httpx_client=self._http_client, + **kwargs, + ) - typed_cached_property = cached_property + def __getattribute__(self, name: str) -> Any: + value = super().__getattribute__(name) + if name.startswith("_") or name in _OWN_ATTRIBUTES: + return value + return _wrap(name, value, super().__getattribute__("_defaults")) + + def copy( + self, + *, + set_default_headers: Mapping[str, str] | None = None, + set_default_query: Mapping[str, object] | None = None, + **overrides: Any, + ) -> AsyncSupermemory: + """A new client with some options changed; the rest, including the connection pool, are kept.""" + options = dict(self._options) + if self._owned_http_client: + options["http_client"] = self._http_client + options["default_headers"] = set_default_headers or { + **options["default_headers"], + **(overrides.pop("default_headers", None) or {}), + } + options["default_query"] = set_default_query or { + **options["default_query"], + **(overrides.pop("default_query", None) or {}), + } + options.update(overrides) + return type(self)(**options) + + with_options = copy + + async def close(self) -> None: + if self._owned_http_client: + await self._http_client.aclose() + + async def __aenter__(self) -> Self: + return self + + async def __aexit__(self, *_: object) -> None: + await self.close() + + +Client = Supermemory +AsyncClient = AsyncSupermemory diff --git a/src/supermemory/_constants.py b/src/supermemory/_constants.py deleted file mode 100644 index 6ddf2c71..00000000 --- a/src/supermemory/_constants.py +++ /dev/null @@ -1,14 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -import httpx - -RAW_RESPONSE_HEADER = "X-Stainless-Raw-Response" -OVERRIDE_CAST_TO_HEADER = "____stainless_override_cast_to" - -# default timeout is 1 minute -DEFAULT_TIMEOUT = httpx.Timeout(timeout=60, connect=5.0) -DEFAULT_MAX_RETRIES = 2 -DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20) - -INITIAL_RETRY_DELAY = 0.5 -MAX_RETRY_DELAY = 8.0 diff --git a/src/supermemory/_default_clients.py b/src/supermemory/_default_clients.py new file mode 100644 index 00000000..f20b4ba7 --- /dev/null +++ b/src/supermemory/_default_clients.py @@ -0,0 +1,30 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx + +SDK_DEFAULT_TIMEOUT = 60 + +try: + import httpx_aiohttp # type: ignore[import-not-found] +except ImportError: + + class DefaultAioHttpClient(httpx.AsyncClient): # type: ignore + def __init__(self, **kwargs: typing.Any) -> None: + raise RuntimeError("To use the aiohttp client, install the aiohttp extra: pip install supermemory[aiohttp]") + +else: + + class DefaultAioHttpClient(httpx_aiohttp.HttpxAiohttpClient): # type: ignore + def __init__(self, **kwargs: typing.Any) -> None: + kwargs.setdefault("timeout", SDK_DEFAULT_TIMEOUT) + kwargs.setdefault("follow_redirects", True) + super().__init__(**kwargs) + + +class DefaultAsyncHttpxClient(httpx.AsyncClient): + def __init__(self, **kwargs: typing.Any) -> None: + kwargs.setdefault("timeout", SDK_DEFAULT_TIMEOUT) + kwargs.setdefault("follow_redirects", True) + super().__init__(**kwargs) diff --git a/src/supermemory/_exceptions.py b/src/supermemory/_exceptions.py deleted file mode 100644 index e4e79d2f..00000000 --- a/src/supermemory/_exceptions.py +++ /dev/null @@ -1,108 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Literal - -import httpx - -__all__ = [ - "BadRequestError", - "AuthenticationError", - "PermissionDeniedError", - "NotFoundError", - "ConflictError", - "UnprocessableEntityError", - "RateLimitError", - "InternalServerError", -] - - -class SupermemoryError(Exception): - pass - - -class APIError(SupermemoryError): - message: str - request: httpx.Request - - body: object | None - """The API response body. - - If the API responded with a valid JSON structure then this property will be the - decoded result. - - If it isn't a valid JSON structure then this will be the raw response. - - If there was no response associated with this error then it will be `None`. - """ - - def __init__(self, message: str, request: httpx.Request, *, body: object | None) -> None: # noqa: ARG002 - super().__init__(message) - self.request = request - self.message = message - self.body = body - - -class APIResponseValidationError(APIError): - response: httpx.Response - status_code: int - - def __init__(self, response: httpx.Response, body: object | None, *, message: str | None = None) -> None: - super().__init__(message or "Data returned by API invalid for expected schema.", response.request, body=body) - self.response = response - self.status_code = response.status_code - - -class APIStatusError(APIError): - """Raised when an API response has a status code of 4xx or 5xx.""" - - response: httpx.Response - status_code: int - - def __init__(self, message: str, *, response: httpx.Response, body: object | None) -> None: - super().__init__(message, response.request, body=body) - self.response = response - self.status_code = response.status_code - - -class APIConnectionError(APIError): - def __init__(self, *, message: str = "Connection error.", request: httpx.Request) -> None: - super().__init__(message, request, body=None) - - -class APITimeoutError(APIConnectionError): - def __init__(self, request: httpx.Request) -> None: - super().__init__(message="Request timed out.", request=request) - - -class BadRequestError(APIStatusError): - status_code: Literal[400] = 400 # pyright: ignore[reportIncompatibleVariableOverride] - - -class AuthenticationError(APIStatusError): - status_code: Literal[401] = 401 # pyright: ignore[reportIncompatibleVariableOverride] - - -class PermissionDeniedError(APIStatusError): - status_code: Literal[403] = 403 # pyright: ignore[reportIncompatibleVariableOverride] - - -class NotFoundError(APIStatusError): - status_code: Literal[404] = 404 # pyright: ignore[reportIncompatibleVariableOverride] - - -class ConflictError(APIStatusError): - status_code: Literal[409] = 409 # pyright: ignore[reportIncompatibleVariableOverride] - - -class UnprocessableEntityError(APIStatusError): - status_code: Literal[422] = 422 # pyright: ignore[reportIncompatibleVariableOverride] - - -class RateLimitError(APIStatusError): - status_code: Literal[429] = 429 # pyright: ignore[reportIncompatibleVariableOverride] - - -class InternalServerError(APIStatusError): - pass diff --git a/src/supermemory/_files.py b/src/supermemory/_files.py deleted file mode 100644 index 5e737df4..00000000 --- a/src/supermemory/_files.py +++ /dev/null @@ -1,173 +0,0 @@ -from __future__ import annotations - -import io -import os -import pathlib -from typing import Sequence, cast, overload -from typing_extensions import TypeVar, TypeGuard - -import anyio - -from ._types import ( - FileTypes, - FileContent, - RequestFiles, - HttpxFileTypes, - Base64FileInput, - HttpxFileContent, - HttpxRequestFiles, -) -from ._utils import is_list, is_mapping, is_tuple_t, is_mapping_t, is_sequence_t - -_T = TypeVar("_T") - - -def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]: - return isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) - - -def is_file_content(obj: object) -> TypeGuard[FileContent]: - return ( - isinstance(obj, bytes) or isinstance(obj, tuple) or isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) - ) - - -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. See https://github.com/supermemoryai/python-sdk/tree/main#file-uploads" - ) from None - - -@overload -def to_httpx_files(files: None) -> None: ... - - -@overload -def to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: ... - - -def to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None: - if files is None: - return None - - if is_mapping_t(files): - files = {key: _transform_file(file) for key, file in files.items()} - elif is_sequence_t(files): - files = [(key, _transform_file(file)) for key, file in files] - else: - raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence") - - return files - - -def _transform_file(file: FileTypes) -> HttpxFileTypes: - if is_file_content(file): - if isinstance(file, os.PathLike): - path = pathlib.Path(file) - return (path.name, path.read_bytes()) - - return file - - if is_tuple_t(file): - return (file[0], read_file_content(file[1]), *file[2:]) - - raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") - - -def read_file_content(file: FileContent) -> HttpxFileContent: - if isinstance(file, os.PathLike): - return pathlib.Path(file).read_bytes() - return file - - -@overload -async def async_to_httpx_files(files: None) -> None: ... - - -@overload -async def async_to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: ... - - -async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None: - if files is None: - return None - - if is_mapping_t(files): - files = {key: await _async_transform_file(file) for key, file in files.items()} - elif is_sequence_t(files): - files = [(key, await _async_transform_file(file)) for key, file in files] - else: - raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence") - - return files - - -async def _async_transform_file(file: FileTypes) -> HttpxFileTypes: - if is_file_content(file): - if isinstance(file, os.PathLike): - path = anyio.Path(file) - return (path.name, await path.read_bytes()) - - return file - - if is_tuple_t(file): - return (file[0], await async_read_file_content(file[1]), *file[2:]) - - raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") - - -async def async_read_file_content(file: FileContent) -> HttpxFileContent: - if isinstance(file, os.PathLike): - return await anyio.Path(file).read_bytes() - - return file - - -def deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]]) -> _T: - """Copy only the containers along the given paths. - - Used to guard against mutation by extract_files without copying the entire structure. - Only dicts and lists that lie on a path are copied; everything else - is returned by reference. - - For example, given paths=[["foo", "files", "file"]] and the structure: - { - "foo": { - "bar": {"baz": {}}, - "files": {"file": } - } - } - The root dict, "foo", and "files" are copied (they lie on the path). - "bar" and "baz" are returned by reference (off the path). - """ - return _deepcopy_with_paths(item, paths, 0) - - -def _deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]], index: int) -> _T: - if not paths: - return item - if is_mapping(item): - key_to_paths: dict[str, list[Sequence[str]]] = {} - for path in paths: - if index < len(path): - key_to_paths.setdefault(path[index], []).append(path) - - # if no path continues through this mapping, it won't be mutated and copying it is redundant - if not key_to_paths: - return item - - result = dict(item) - for key, subpaths in key_to_paths.items(): - if key in result: - result[key] = _deepcopy_with_paths(result[key], subpaths, index + 1) - return cast(_T, result) - if is_list(item): - array_paths = [path for path in paths if index < len(path) and path[index] == ""] - - # if no path expects a list here, nothing will be mutated inside it - return by reference - if not array_paths: - return cast(_T, item) - return cast(_T, [_deepcopy_with_paths(entry, array_paths, index + 1) for entry in item]) - return item diff --git a/src/supermemory/_models.py b/src/supermemory/_models.py deleted file mode 100644 index 8c5ab260..00000000 --- a/src/supermemory/_models.py +++ /dev/null @@ -1,952 +0,0 @@ -from __future__ import annotations - -import os -import inspect -import weakref -from typing import ( - IO, - TYPE_CHECKING, - Any, - Type, - Union, - Generic, - TypeVar, - Callable, - Iterable, - Optional, - AsyncIterable, - cast, -) -from datetime import date, datetime -from typing_extensions import ( - List, - Unpack, - Literal, - ClassVar, - Protocol, - Required, - Annotated, - ParamSpec, - TypeAlias, - TypedDict, - TypeGuard, - final, - override, - runtime_checkable, -) - -import pydantic -from pydantic.fields import FieldInfo - -from ._types import ( - Body, - IncEx, - Query, - ModelT, - Headers, - Timeout, - NotGiven, - AnyMapping, - HttpxRequestFiles, -) -from ._utils import ( - PropertyInfo, - is_list, - is_given, - json_safe, - lru_cache, - is_mapping, - parse_date, - coerce_boolean, - parse_datetime, - strip_not_given, - extract_type_arg, - is_annotated_type, - is_type_alias_type, - strip_annotated_type, -) -from ._compat import ( - PYDANTIC_V1, - ConfigDict, - GenericModel as BaseGenericModel, - get_args, - is_union, - parse_obj, - get_origin, - is_literal_type, - get_model_config, - get_model_fields, - field_get_default, -) -from ._constants import RAW_RESPONSE_HEADER - -if TYPE_CHECKING: - from pydantic import GetCoreSchemaHandler, ValidatorFunctionWrapHandler - from pydantic_core import CoreSchema, core_schema - from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema -else: - try: - from pydantic_core import CoreSchema, core_schema - except ImportError: - CoreSchema = None - core_schema = None - -__all__ = ["BaseModel", "GenericModel"] - -_T = TypeVar("_T") -_BaseModelT = TypeVar("_BaseModelT", bound="BaseModel") - -P = ParamSpec("P") - - -@runtime_checkable -class _ConfigProtocol(Protocol): - allow_population_by_field_name: bool - - -class BaseModel(pydantic.BaseModel): - if PYDANTIC_V1: - - @property - @override - def model_fields_set(self) -> set[str]: - # a forwards-compat shim for pydantic v2 - return self.__fields_set__ # type: ignore - - class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] - extra: Any = pydantic.Extra.allow # type: ignore - else: - model_config: ClassVar[ConfigDict] = ConfigDict( - extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true")) - ) - - def to_dict( - self, - *, - mode: Literal["json", "python"] = "python", - use_api_names: bool = True, - exclude_unset: bool = True, - exclude_defaults: bool = False, - exclude_none: bool = False, - warnings: bool = True, - ) -> dict[str, object]: - """Recursively generate a dictionary representation of the model, optionally specifying which fields to include or exclude. - - By default, fields that were not set by the API will not be included, - and keys will match the API response, *not* the property names from the model. - - For example, if the API responds with `"fooBar": true` but we've defined a `foo_bar: bool` property, - the output will use the `"fooBar"` key (unless `use_api_names=False` is passed). - - Args: - mode: - If mode is 'json', the dictionary will only contain JSON serializable types. e.g. `datetime` will be turned into a string, `"2024-3-22T18:11:19.117000Z"`. - If mode is 'python', the dictionary may contain any Python objects. e.g. `datetime(2024, 3, 22)` - - use_api_names: Whether to use the key that the API responded with or the property name. Defaults to `True`. - 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 from the output. - exclude_none: Whether to exclude fields that have a value of `None` from the output. - warnings: Whether to log warnings when invalid fields are encountered. This is only supported in Pydantic v2. - """ - return self.model_dump( - mode=mode, - by_alias=use_api_names, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - warnings=warnings, - ) - - def to_json( - self, - *, - indent: int | None = 2, - use_api_names: bool = True, - exclude_unset: bool = True, - exclude_defaults: bool = False, - exclude_none: bool = False, - warnings: bool = True, - ) -> str: - """Generates a JSON string representing this model as it would be received from or sent to the API (but with indentation). - - By default, fields that were not set by the API will not be included, - and keys will match the API response, *not* the property names from the model. - - For example, if the API responds with `"fooBar": true` but we've defined a `foo_bar: bool` property, - the output will use the `"fooBar"` key (unless `use_api_names=False` is passed). - - Args: - indent: Indentation to use in the JSON output. If `None` is passed, the output will be compact. Defaults to `2` - use_api_names: Whether to use the key that the API responded with or the property name. Defaults to `True`. - exclude_unset: Whether to exclude fields that have not been explicitly set. - exclude_defaults: Whether to exclude fields that have the default value. - exclude_none: Whether to exclude fields that have a value of `None`. - warnings: Whether to show any warnings that occurred during serialization. This is only supported in Pydantic v2. - """ - return self.model_dump_json( - indent=indent, - by_alias=use_api_names, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - warnings=warnings, - ) - - @override - def __str__(self) -> str: - # mypy complains about an invalid self arg - return f"{self.__repr_name__()}({self.__repr_str__(', ')})" # type: ignore[misc] - - # Override the 'construct' method in a way that supports recursive parsing without validation. - # Based on https://github.com/samuelcolvin/pydantic/issues/1168#issuecomment-817742836. - @classmethod - @override - def construct( # pyright: ignore[reportIncompatibleMethodOverride] - __cls: Type[ModelT], - _fields_set: set[str] | None = None, - **values: object, - ) -> ModelT: - m = __cls.__new__(__cls) - fields_values: dict[str, object] = {} - - config = get_model_config(__cls) - populate_by_name = ( - config.allow_population_by_field_name - if isinstance(config, _ConfigProtocol) - else config.get("populate_by_name") - ) - - if _fields_set is None: - _fields_set = set() - - model_fields = get_model_fields(__cls) - for name, field in model_fields.items(): - key = field.alias - if key is None or (key not in values and populate_by_name): - key = name - - if key in values: - fields_values[name] = _construct_field(value=values[key], field=field, key=key) - _fields_set.add(name) - else: - fields_values[name] = field_get_default(field) - - extra_field_type = _get_extra_fields_type(__cls) - - _extra = {} - for key, value in values.items(): - if key not in model_fields: - parsed = construct_type(value=value, type_=extra_field_type) if extra_field_type is not None else value - - if PYDANTIC_V1: - _fields_set.add(key) - fields_values[key] = parsed - else: - _extra[key] = parsed - - object.__setattr__(m, "__dict__", fields_values) - - if PYDANTIC_V1: - # init_private_attributes() does not exist in v2 - m._init_private_attributes() # type: ignore - - # copied from Pydantic v1's `construct()` method - object.__setattr__(m, "__fields_set__", _fields_set) - else: - # these properties are copied from Pydantic's `model_construct()` method - object.__setattr__(m, "__pydantic_private__", None) - object.__setattr__(m, "__pydantic_extra__", _extra) - object.__setattr__(m, "__pydantic_fields_set__", _fields_set) - - return m - - if not TYPE_CHECKING: - # type checkers incorrectly complain about this assignment - # because the type signatures are technically different - # although not in practice - model_construct = construct - - if PYDANTIC_V1: - # we define aliases for some of the new pydantic v2 methods so - # that we can just document these methods without having to specify - # a specific pydantic version as some users may not know which - # pydantic version they are currently using - - @override - def model_dump( - self, - *, - 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, - 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 - - Generate a dictionary representation of the model, optionally specifying which fields to include or exclude. - - Args: - mode: The mode in which `to_python` should run. - 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 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. - """ - if mode not in {"json", "python"}: - raise ValueError("mode must be either 'json' or 'python'") - if round_trip != False: - raise ValueError("round_trip is only supported in Pydantic v2") - if warnings != True: - raise ValueError("warnings is only supported in Pydantic v2") - if context is not None: - raise ValueError("context is only supported in Pydantic v2") - if serialize_as_any != False: - 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, - by_alias=by_alias if by_alias is not None else False, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) - - return cast("dict[str, Any]", json_safe(dumped)) if mode == "json" else dumped - - @override - 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, - fallback: Callable[[Any], Any] | None = None, - serialize_as_any: bool = False, - ) -> str: - """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump_json - - Generates a JSON representation of the model using Pydantic's `to_json` method. - - Args: - indent: Indentation to use in the JSON output. If None is passed, the output will be compact. - include: Field(s) to include in the JSON output. Can take either a string or set of strings. - exclude: Field(s) to exclude from the JSON output. Can take either a string or set of strings. - by_alias: Whether to serialize using field aliases. - exclude_unset: Whether to exclude fields that have not been explicitly set. - exclude_defaults: Whether to exclude fields that have the default value. - exclude_none: Whether to exclude fields that have a value of `None`. - round_trip: Whether to use serialization/deserialization between JSON and class instance. - warnings: Whether to show any warnings that occurred during serialization. - - Returns: - A JSON string representation of the model. - """ - if round_trip != False: - raise ValueError("round_trip is only supported in Pydantic v2") - if warnings != True: - raise ValueError("warnings is only supported in Pydantic v2") - if context is not None: - raise ValueError("context is only supported in Pydantic v2") - if serialize_as_any != False: - 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, - exclude=exclude, - by_alias=by_alias if by_alias is not None else False, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) - - -class _EagerIterable(list[_T], Generic[_T]): - """ - Accepts any Iterable[T] input (including generators), consumes it - eagerly, and validates all items upfront. - - Validation preserves the original container type where possible - (e.g. a set[T] stays a set[T]). Serialization (model_dump / JSON) - always emits a list — round-tripping through model_dump() will not - restore the original container type. - """ - - @classmethod - def __get_pydantic_core_schema__( - cls, - source_type: Any, - handler: GetCoreSchemaHandler, - ) -> CoreSchema: - (item_type,) = get_args(source_type) or (Any,) - item_schema: CoreSchema = handler.generate_schema(item_type) - list_of_items_schema: CoreSchema = core_schema.list_schema(item_schema) - - return core_schema.no_info_wrap_validator_function( - cls._validate, - list_of_items_schema, - serialization=core_schema.plain_serializer_function_ser_schema( - cls._serialize, - info_arg=False, - ), - ) - - @staticmethod - def _validate(v: Iterable[_T], handler: "ValidatorFunctionWrapHandler") -> Any: - original_type: type[Any] = type(v) - - # Normalize to list so list_schema can validate each item - if isinstance(v, list): - items: list[_T] = v - else: - try: - items = list(v) - except TypeError as e: - raise TypeError("Value is not iterable") from e - - # Validate items against the inner schema - validated: list[_T] = handler(items) - - # Reconstruct original container type - if original_type is list: - return validated - # str(list) produces the list's repr, not a string built from items, - # so skip reconstruction for str and its subclasses. - if issubclass(original_type, str): - return validated - try: - return original_type(validated) - except (TypeError, ValueError): - # If the type cannot be reconstructed, just return the validated list - return validated - - @staticmethod - def _serialize(v: Iterable[_T]) -> list[_T]: - """Always serialize as a list so Pydantic's JSON encoder is happy.""" - if isinstance(v, list): - return v - return list(v) - - -EagerIterable: TypeAlias = Annotated[Iterable[_T], _EagerIterable] - - -def _construct_field(value: object, field: FieldInfo, key: str) -> object: - if value is None: - return field_get_default(field) - - if PYDANTIC_V1: - type_ = cast(type, field.outer_type_) # type: ignore - else: - type_ = field.annotation # type: ignore - - if type_ is None: - raise RuntimeError(f"Unexpected field type is None for {key}") - - return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None)) - - -def _get_extra_fields_type(cls: type[pydantic.BaseModel]) -> type | None: - if PYDANTIC_V1: - # TODO - return None - - schema = cls.__pydantic_core_schema__ - if schema["type"] == "model": - fields = schema["schema"] - if fields["type"] == "model-fields": - extras = fields.get("extras_schema") - if extras and "cls" in extras: - # mypy can't narrow the type - return extras["cls"] # type: ignore[no-any-return] - - return None - - -def is_basemodel(type_: type) -> bool: - """Returns whether or not the given type is either a `BaseModel` or a union of `BaseModel`""" - if is_union(type_): - for variant in get_args(type_): - if is_basemodel(variant): - return True - - return False - - return is_basemodel_type(type_) - - -def is_basemodel_type(type_: type) -> TypeGuard[type[BaseModel] | type[GenericModel]]: - origin = get_origin(type_) or type_ - if not inspect.isclass(origin): - return False - return issubclass(origin, BaseModel) or issubclass(origin, GenericModel) - - -def build( - base_model_cls: Callable[P, _BaseModelT], - *args: P.args, - **kwargs: P.kwargs, -) -> _BaseModelT: - """Construct a BaseModel class without validation. - - This is useful for cases where you need to instantiate a `BaseModel` - from an API response as this provides type-safe params which isn't supported - by helpers like `construct_type()`. - - ```py - build(MyModel, my_field_a="foo", my_field_b=123) - ``` - """ - if args: - raise TypeError( - "Received positional arguments which are not supported; Keyword arguments must be used instead", - ) - - return cast(_BaseModelT, construct_type(type_=base_model_cls, value=kwargs)) - - -def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T: - """Loose coercion to the expected type with construction of nested values. - - Note: the returned value from this function is not guaranteed to match the - given type. - """ - return cast(_T, construct_type(value=value, type_=type_)) - - -def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]] = None) -> object: - """Loose coercion to the expected type with construction of nested values. - - If the given value does not match the expected type then it is returned as-is. - """ - - # store a reference to the original type we were given before we extract any inner - # types so that we can properly resolve forward references in `TypeAliasType` annotations - original_type = None - - # we allow `object` as the input type because otherwise, passing things like - # `Literal['value']` will be reported as a type error by type checkers - type_ = cast("type[object]", type_) - if is_type_alias_type(type_): - original_type = type_ # type: ignore[unreachable] - type_ = type_.__value__ # type: ignore[unreachable] - - # unwrap `Annotated[T, ...]` -> `T` - if metadata is not None and len(metadata) > 0: - meta: tuple[Any, ...] = tuple(metadata) - elif is_annotated_type(type_): - meta = get_args(type_)[1:] - type_ = extract_type_arg(type_, 0) - else: - meta = tuple() - - # we need to use the origin class for any types that are subscripted generics - # e.g. Dict[str, object] - origin = get_origin(type_) or type_ - args = get_args(type_) - - if is_union(origin): - try: - return validate_type(type_=cast("type[object]", original_type or type_), value=value) - except Exception: - pass - - # if the type is a discriminated union then we want to construct the right variant - # in the union, even if the data doesn't match exactly, otherwise we'd break code - # that relies on the constructed class types, e.g. - # - # class FooType: - # kind: Literal['foo'] - # value: str - # - # class BarType: - # kind: Literal['bar'] - # value: int - # - # without this block, if the data we get is something like `{'kind': 'bar', 'value': 'foo'}` then - # we'd end up constructing `FooType` when it should be `BarType`. - discriminator = _build_discriminated_union_meta(union=type_, meta_annotations=meta) - if discriminator and is_mapping(value): - variant_value = value.get(discriminator.field_alias_from or discriminator.field_name) - if variant_value and isinstance(variant_value, str): - variant_type = discriminator.mapping.get(variant_value) - if variant_type: - return construct_type(type_=variant_type, value=value) - - # if the data is not valid, use the first variant that doesn't fail while deserializing - for variant in args: - try: - return construct_type(value=value, type_=variant) - except Exception: - continue - - raise RuntimeError(f"Could not convert data into a valid instance of {type_}") - - if origin == dict: - if not is_mapping(value): - return value - - _, items_type = get_args(type_) # Dict[_, items_type] - return {key: construct_type(value=item, type_=items_type) for key, item in value.items()} - - if ( - not is_literal_type(type_) - and inspect.isclass(origin) - and (issubclass(origin, BaseModel) or issubclass(origin, GenericModel)) - ): - if is_list(value): - return [cast(Any, type_).construct(**entry) if is_mapping(entry) else entry for entry in value] - - if is_mapping(value): - if issubclass(type_, BaseModel): - return type_.construct(**value) # type: ignore[arg-type] - - return cast(Any, type_).construct(**value) - - if origin == list: - if not is_list(value): - return value - - inner_type = args[0] # List[inner_type] - return [construct_type(value=entry, type_=inner_type) for entry in value] - - if origin == float: - if isinstance(value, int): - coerced = float(value) - if coerced != value: - return value - return coerced - - return value - - if type_ == datetime: - try: - return parse_datetime(value) # type: ignore - except Exception: - return value - - if type_ == date: - try: - return parse_date(value) # type: ignore - except Exception: - return value - - return value - - -@runtime_checkable -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. - - ```py - class Foo(BaseModel): - type: Literal['foo'] - ``` - - Will result in field_name='type' - """ - - field_alias_from: str | None - """The name of the discriminator field in the API response, e.g. - - ```py - class Foo(BaseModel): - type: Literal['foo'] = Field(alias='type_from_api') - ``` - - Will result in field_alias_from='type_from_api' - """ - - mapping: dict[str, type] - """Mapping of discriminator value to variant type, e.g. - - {'foo': FooVariant, 'bar': BarVariant} - """ - - def __init__( - self, - *, - mapping: dict[str, type], - discriminator_field: str, - discriminator_alias: str | None, - ) -> None: - self.mapping = mapping - self.field_name = discriminator_field - self.field_alias_from = discriminator_alias - - -def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, ...]) -> DiscriminatorDetails | None: - cached = DISCRIMINATOR_CACHE.get(union) - if cached is not None: - return cached - - discriminator_field_name: str | None = None - - for annotation in meta_annotations: - if isinstance(annotation, PropertyInfo) and annotation.discriminator is not None: - discriminator_field_name = annotation.discriminator - break - - if not discriminator_field_name: - return None - - mapping: dict[str, type] = {} - discriminator_alias: str | None = None - - for variant in get_args(union): - variant = strip_annotated_type(variant) - if is_basemodel_type(variant): - if PYDANTIC_V1: - field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] - if not field_info: - continue - - # Note: if one variant defines an alias then they all should - discriminator_alias = field_info.alias - - if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation): - for entry in get_args(annotation): - if isinstance(entry, str): - mapping[entry] = variant - else: - field = _extract_field_schema_pv2(variant, discriminator_field_name) - if not field: - continue - - # Note: if one variant defines an alias then they all should - discriminator_alias = field.get("serialization_alias") - - field_schema = field["schema"] - - if field_schema["type"] == "literal": - for entry in cast("LiteralSchema", field_schema)["expected"]: - if isinstance(entry, str): - mapping[entry] = variant - - if not mapping: - return None - - details = DiscriminatorDetails( - mapping=mapping, - discriminator_field=discriminator_field_name, - discriminator_alias=discriminator_alias, - ) - DISCRIMINATOR_CACHE.setdefault(union, details) - return details - - -def _extract_field_schema_pv2(model: type[BaseModel], field_name: str) -> ModelField | None: - schema = model.__pydantic_core_schema__ - if schema["type"] == "definitions": - schema = schema["schema"] - - if schema["type"] != "model": - return None - - schema = cast("ModelSchema", schema) - fields_schema = schema["schema"] - if fields_schema["type"] != "model-fields": - return None - - fields_schema = cast("ModelFieldsSchema", fields_schema) - field = fields_schema["fields"].get(field_name) - if not field: - return None - - return cast("ModelField", field) # pyright: ignore[reportUnnecessaryCast] - - -def validate_type(*, type_: type[_T], value: object) -> _T: - """Strict validation that the given value matches the expected type""" - if inspect.isclass(type_) and issubclass(type_, pydantic.BaseModel): - return cast(_T, parse_obj(type_, value)) - - return cast(_T, _validate_non_model_type(type_=type_, value=value)) - - -def set_pydantic_config(typ: Any, config: pydantic.ConfigDict) -> None: - """Add a pydantic config for the given type. - - Note: this is a no-op on Pydantic v1. - """ - setattr(typ, "__pydantic_config__", config) # noqa: B010 - - -# our use of subclassing here causes weirdness for type checkers, -# so we just pretend that we don't subclass -if TYPE_CHECKING: - GenericModel = BaseModel -else: - - class GenericModel(BaseGenericModel, BaseModel): - pass - - -if not PYDANTIC_V1: - from pydantic import TypeAdapter as _TypeAdapter - - _CachedTypeAdapter = cast("TypeAdapter[object]", lru_cache(maxsize=None)(_TypeAdapter)) - - if TYPE_CHECKING: - from pydantic import TypeAdapter - else: - TypeAdapter = _CachedTypeAdapter - - def _validate_non_model_type(*, type_: type[_T], value: object) -> _T: - return TypeAdapter(type_).validate_python(value) - -elif not TYPE_CHECKING: # TODO: condition is weird - - class RootModel(GenericModel, Generic[_T]): - """Used as a placeholder to easily convert runtime types to a Pydantic format - to provide validation. - - For example: - ```py - validated = RootModel[int](__root__="5").__root__ - # validated: 5 - ``` - """ - - __root__: _T - - def _validate_non_model_type(*, type_: type[_T], value: object) -> _T: - model = _create_pydantic_model(type_).validate(value) - return cast(_T, model.__root__) - - def _create_pydantic_model(type_: _T) -> Type[RootModel[_T]]: - return RootModel[type_] # type: ignore - - -class FinalRequestOptionsInput(TypedDict, total=False): - method: Required[str] - url: Required[str] - params: Query - headers: Headers - max_retries: int - timeout: float | Timeout | None - files: HttpxRequestFiles | None - idempotency_key: str - content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] - json_data: Body - extra_json: AnyMapping - follow_redirects: bool - - -@final -class FinalRequestOptions(pydantic.BaseModel): - method: str - url: str - params: Query = {} - headers: Union[Headers, NotGiven] = NotGiven() - max_retries: Union[int, NotGiven] = NotGiven() - timeout: Union[float, Timeout, None, NotGiven] = NotGiven() - files: Union[HttpxRequestFiles, None] = None - idempotency_key: Union[str, None] = None - post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() - follow_redirects: Union[bool, None] = None - - content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None - # It should be noted that we cannot use `json` here as that would override - # a BaseModel method in an incompatible fashion. - json_data: Union[Body, None] = None - extra_json: Union[AnyMapping, None] = None - - if PYDANTIC_V1: - - class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] - arbitrary_types_allowed: bool = True - else: - model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True) - - def get_max_retries(self, max_retries: int) -> int: - if isinstance(self.max_retries, NotGiven): - return max_retries - return self.max_retries - - def _strip_raw_response_header(self) -> None: - if not is_given(self.headers): - return - - if self.headers.get(RAW_RESPONSE_HEADER): - self.headers = {**self.headers} - self.headers.pop(RAW_RESPONSE_HEADER) - - # override the `construct` method so that we can run custom transformations. - # this is necessary as we don't want to do any actual runtime type checking - # (which means we can't use validators) but we do want to ensure that `NotGiven` - # values are not present - # - # type ignore required because we're adding explicit types to `**values` - @classmethod - def construct( # type: ignore - cls, - _fields_set: set[str] | None = None, - **values: Unpack[FinalRequestOptionsInput], - ) -> FinalRequestOptions: - kwargs: dict[str, Any] = { - # we unconditionally call `strip_not_given` on any value - # as it will just ignore any non-mapping types - key: strip_not_given(value) - for key, value in values.items() - } - if PYDANTIC_V1: - return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated] - return super().model_construct(_fields_set, **kwargs) - - if not TYPE_CHECKING: - # type checkers incorrectly complain about this assignment - model_construct = construct diff --git a/src/supermemory/_qs.py b/src/supermemory/_qs.py deleted file mode 100644 index 4127c19c..00000000 --- a/src/supermemory/_qs.py +++ /dev/null @@ -1,149 +0,0 @@ -from __future__ import annotations - -from typing import Any, List, Tuple, Union, Mapping, TypeVar -from urllib.parse import parse_qs, urlencode -from typing_extensions import get_args - -from ._types import NotGiven, ArrayFormat, NestedFormat, not_given -from ._utils import flatten - -_T = TypeVar("_T") - -PrimitiveData = Union[str, int, float, bool, None] -# this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"] -# https://github.com/microsoft/pyright/issues/3555 -Data = Union[PrimitiveData, List[Any], Tuple[Any], "Mapping[str, Any]"] -Params = Mapping[str, Data] - - -class Querystring: - array_format: ArrayFormat - nested_format: NestedFormat - - def __init__( - self, - *, - array_format: ArrayFormat = "repeat", - nested_format: NestedFormat = "brackets", - ) -> None: - self.array_format = array_format - self.nested_format = nested_format - - def parse(self, query: str) -> Mapping[str, object]: - # Note: custom format syntax is not supported yet - return parse_qs(query) - - def stringify( - self, - params: Params, - *, - array_format: ArrayFormat | NotGiven = not_given, - nested_format: NestedFormat | NotGiven = not_given, - ) -> str: - return urlencode( - self.stringify_items( - params, - array_format=array_format, - nested_format=nested_format, - ) - ) - - def stringify_items( - self, - params: Params, - *, - array_format: ArrayFormat | NotGiven = not_given, - nested_format: NestedFormat | NotGiven = not_given, - ) -> list[tuple[str, str]]: - opts = Options( - qs=self, - array_format=array_format, - nested_format=nested_format, - ) - return flatten([self._stringify_item(key, value, opts) for key, value in params.items()]) - - def _stringify_item( - self, - key: str, - value: Data, - opts: Options, - ) -> list[tuple[str, str]]: - if isinstance(value, Mapping): - items: list[tuple[str, str]] = [] - nested_format = opts.nested_format - for subkey, subvalue in value.items(): - items.extend( - self._stringify_item( - # TODO: error if unknown format - f"{key}.{subkey}" if nested_format == "dots" else f"{key}[{subkey}]", - subvalue, - opts, - ) - ) - return items - - if isinstance(value, (list, tuple)): - array_format = opts.array_format - if array_format == "comma": - return [ - ( - key, - ",".join(self._primitive_value_to_str(item) for item in value if item is not None), - ), - ] - elif array_format == "repeat": - items = [] - for item in value: - items.extend(self._stringify_item(key, item, opts)) - return items - elif array_format == "indices": - items = [] - for i, item in enumerate(value): - items.extend(self._stringify_item(f"{key}[{i}]", item, opts)) - return items - elif array_format == "brackets": - items = [] - key = key + "[]" - for item in value: - items.extend(self._stringify_item(key, item, opts)) - return items - else: - raise NotImplementedError( - f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}" - ) - - serialised = self._primitive_value_to_str(value) - if not serialised: - return [] - return [(key, serialised)] - - def _primitive_value_to_str(self, value: PrimitiveData) -> str: - # copied from httpx - if value is True: - return "true" - elif value is False: - return "false" - elif value is None: - return "" - return str(value) - - -_qs = Querystring() -parse = _qs.parse -stringify = _qs.stringify -stringify_items = _qs.stringify_items - - -class Options: - array_format: ArrayFormat - nested_format: NestedFormat - - def __init__( - self, - qs: Querystring = _qs, - *, - array_format: ArrayFormat | NotGiven = not_given, - nested_format: NestedFormat | NotGiven = not_given, - ) -> None: - self.array_format = qs.array_format if isinstance(array_format, NotGiven) else array_format - self.nested_format = qs.nested_format if isinstance(nested_format, NotGiven) else nested_format diff --git a/src/supermemory/_resource.py b/src/supermemory/_resource.py deleted file mode 100644 index b6aa67ef..00000000 --- a/src/supermemory/_resource.py +++ /dev/null @@ -1,43 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import time -from typing import TYPE_CHECKING - -import anyio - -if TYPE_CHECKING: - from ._client import Supermemory, AsyncSupermemory - - -class SyncAPIResource: - _client: Supermemory - - def __init__(self, client: Supermemory) -> None: - self._client = client - self._get = client.get - self._post = client.post - self._patch = client.patch - self._put = client.put - self._delete = client.delete - self._get_api_list = client.get_api_list - - def _sleep(self, seconds: float) -> None: - time.sleep(seconds) - - -class AsyncAPIResource: - _client: AsyncSupermemory - - def __init__(self, client: AsyncSupermemory) -> None: - self._client = client - self._get = client.get - self._post = client.post - self._patch = client.patch - self._put = client.put - self._delete = client.delete - self._get_api_list = client.get_api_list - - async def _sleep(self, seconds: float) -> None: - await anyio.sleep(seconds) diff --git a/src/supermemory/_response.py b/src/supermemory/_response.py deleted file mode 100644 index fb8ba932..00000000 --- a/src/supermemory/_response.py +++ /dev/null @@ -1,835 +0,0 @@ -from __future__ import annotations - -import os -import inspect -import logging -import datetime -import functools -from types import TracebackType -from typing import ( - TYPE_CHECKING, - Any, - Union, - Generic, - TypeVar, - Callable, - Iterator, - AsyncIterator, - cast, - overload, -) -from typing_extensions import Awaitable, ParamSpec, override, get_origin - -import anyio -import httpx -import pydantic - -from ._types import NoneType -from ._utils import is_given, extract_type_arg, is_annotated_type, is_type_alias_type, extract_type_var_from_base -from ._models import BaseModel, is_basemodel -from ._constants import RAW_RESPONSE_HEADER, OVERRIDE_CAST_TO_HEADER -from ._streaming import Stream, AsyncStream, is_stream_class_type, extract_stream_chunk_type -from ._exceptions import SupermemoryError, APIResponseValidationError - -if TYPE_CHECKING: - from ._models import FinalRequestOptions - from ._base_client import BaseClient - - -P = ParamSpec("P") -R = TypeVar("R") -_T = TypeVar("_T") -_APIResponseT = TypeVar("_APIResponseT", bound="APIResponse[Any]") -_AsyncAPIResponseT = TypeVar("_AsyncAPIResponseT", bound="AsyncAPIResponse[Any]") - -log: logging.Logger = logging.getLogger(__name__) - - -class BaseAPIResponse(Generic[R]): - _cast_to: type[R] - _client: BaseClient[Any, Any] - _parsed_by_type: dict[type[Any], Any] - _is_sse_stream: bool - _stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None - _options: FinalRequestOptions - - http_response: httpx.Response - - retries_taken: int - """The number of retries made. If no retries happened this will be `0`""" - - def __init__( - self, - *, - raw: httpx.Response, - cast_to: type[R], - client: BaseClient[Any, Any], - stream: bool, - stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, - options: FinalRequestOptions, - retries_taken: int = 0, - ) -> None: - self._cast_to = cast_to - self._client = client - self._parsed_by_type = {} - self._is_sse_stream = stream - self._stream_cls = stream_cls - self._options = options - self.http_response = raw - self.retries_taken = retries_taken - - @property - def headers(self) -> httpx.Headers: - return self.http_response.headers - - @property - def http_request(self) -> httpx.Request: - """Returns the httpx Request instance associated with the current response.""" - return self.http_response.request - - @property - def status_code(self) -> int: - return self.http_response.status_code - - @property - def url(self) -> httpx.URL: - """Returns the URL for which the request was made.""" - return self.http_response.url - - @property - def method(self) -> str: - return self.http_request.method - - @property - def http_version(self) -> str: - return self.http_response.http_version - - @property - def elapsed(self) -> datetime.timedelta: - """The time taken for the complete request/response cycle to complete.""" - return self.http_response.elapsed - - @property - def is_closed(self) -> bool: - """Whether or not the response body has been closed. - - If this is False then there is response data that has not been read yet. - You must either fully consume the response body or call `.close()` - before discarding the response to prevent resource leaks. - """ - return self.http_response.is_closed - - @override - def __repr__(self) -> str: - return ( - f"<{self.__class__.__name__} [{self.status_code} {self.http_response.reason_phrase}] type={self._cast_to}>" - ) - - def _parse(self, *, to: type[_T] | None = None) -> R | _T: - cast_to = to if to is not None else self._cast_to - - # unwrap `TypeAlias('Name', T)` -> `T` - if is_type_alias_type(cast_to): - cast_to = cast_to.__value__ # type: ignore[unreachable] - - # unwrap `Annotated[T, ...]` -> `T` - if cast_to and is_annotated_type(cast_to): - cast_to = extract_type_arg(cast_to, 0) - - origin = get_origin(cast_to) or cast_to - - if self._is_sse_stream: - if to: - if not is_stream_class_type(to): - raise TypeError(f"Expected custom parse type to be a subclass of {Stream} or {AsyncStream}") - - return cast( - _T, - to( - cast_to=extract_stream_chunk_type( - to, - failure_message="Expected custom stream type to be passed with a type argument, e.g. Stream[ChunkType]", - ), - response=self.http_response, - client=cast(Any, self._client), - options=self._options, - ), - ) - - if self._stream_cls: - return cast( - R, - self._stream_cls( - cast_to=extract_stream_chunk_type(self._stream_cls), - response=self.http_response, - client=cast(Any, self._client), - options=self._options, - ), - ) - - stream_cls = cast("type[Stream[Any]] | type[AsyncStream[Any]] | None", self._client._default_stream_cls) - if stream_cls is None: - raise MissingStreamClassError() - - return cast( - R, - stream_cls( - cast_to=cast_to, - response=self.http_response, - client=cast(Any, self._client), - options=self._options, - ), - ) - - if cast_to is NoneType: - return cast(R, None) - - response = self.http_response - if cast_to == str: - return cast(R, response.text) - - if cast_to == bytes: - return cast(R, response.content) - - if cast_to == int: - return cast(R, int(response.text)) - - if cast_to == float: - return cast(R, float(response.text)) - - if cast_to == bool: - return cast(R, response.text.lower() == "true") - - if origin == APIResponse: - raise RuntimeError("Unexpected state - cast_to is `APIResponse`") - - if inspect.isclass(origin) and issubclass(origin, httpx.Response): - # Because of the invariance of our ResponseT TypeVar, users can subclass httpx.Response - # and pass that class to our request functions. We cannot change the variance to be either - # covariant or contravariant as that makes our usage of ResponseT illegal. We could construct - # the response class ourselves but that is something that should be supported directly in httpx - # as it would be easy to incorrectly construct the Response object due to the multitude of arguments. - if cast_to != httpx.Response: - raise ValueError(f"Subclasses of httpx.Response cannot be passed to `cast_to`") - return cast(R, response) - - if ( - inspect.isclass( - origin # pyright: ignore[reportUnknownArgumentType] - ) - and not issubclass(origin, BaseModel) - and issubclass(origin, pydantic.BaseModel) - ): - raise TypeError( - "Pydantic models must subclass our base model type, e.g. `from supermemory import BaseModel`" - ) - - if ( - cast_to is not object - and not origin is list - and not origin is dict - and not origin is Union - and not issubclass(origin, BaseModel) - ): - raise RuntimeError( - f"Unsupported type, expected {cast_to} to be a subclass of {BaseModel}, {dict}, {list}, {Union}, {NoneType}, {str} or {httpx.Response}." - ) - - # split is required to handle cases where additional information is included - # in the response, e.g. application/json; charset=utf-8 - content_type, *_ = response.headers.get("content-type", "*").split(";") - if not content_type.endswith("json"): - if is_basemodel(cast_to): - try: - data = response.json() - except Exception as exc: - log.debug("Could not read JSON from response data due to %s - %s", type(exc), exc) - else: - return self._client._process_response_data( - data=data, - cast_to=cast_to, # type: ignore - response=response, - ) - - if self._client._strict_response_validation: - raise APIResponseValidationError( - response=response, - message=f"Expected Content-Type response header to be `application/json` but received `{content_type}` instead.", - body=response.text, - ) - - # If the API responds with content that isn't JSON then we just return - # the (decoded) text without performing any parsing so that you can still - # handle the response however you need to. - return response.text # type: ignore - - data = response.json() - - return self._client._process_response_data( - data=data, - cast_to=cast_to, # type: ignore - response=response, - ) - - -class APIResponse(BaseAPIResponse[R]): - @overload - def parse(self, *, to: type[_T]) -> _T: ... - - @overload - def parse(self) -> R: ... - - def parse(self, *, to: type[_T] | None = None) -> R | _T: - """Returns the rich python representation of this response's data. - - For lower-level control, see `.read()`, `.json()`, `.iter_bytes()`. - - You can customise the type that the response is parsed into through - the `to` argument, e.g. - - ```py - from supermemory import BaseModel - - - class MyModel(BaseModel): - foo: str - - - obj = response.parse(to=MyModel) - print(obj.foo) - ``` - - We support parsing: - - `BaseModel` - - `dict` - - `list` - - `Union` - - `str` - - `int` - - `float` - - `httpx.Response` - """ - cache_key = to if to is not None else self._cast_to - cached = self._parsed_by_type.get(cache_key) - if cached is not None: - return cached # type: ignore[no-any-return] - - if not self._is_sse_stream: - self.read() - - parsed = self._parse(to=to) - if is_given(self._options.post_parser): - parsed = self._options.post_parser(parsed) - - self._parsed_by_type[cache_key] = parsed - return parsed - - def read(self) -> bytes: - """Read and return the binary response content.""" - try: - return self.http_response.read() - except httpx.StreamConsumed as exc: - # The default error raised by httpx isn't very - # helpful in our case so we re-raise it with - # a different error message. - raise StreamAlreadyConsumed() from exc - - def text(self) -> str: - """Read and decode the response content into a string.""" - self.read() - return self.http_response.text - - def json(self) -> object: - """Read and decode the JSON response content.""" - self.read() - return self.http_response.json() - - def close(self) -> None: - """Close the response and release the connection. - - Automatically called if the response body is read to completion. - """ - self.http_response.close() - - def iter_bytes(self, chunk_size: int | None = None) -> Iterator[bytes]: - """ - A byte-iterator over the decoded response content. - - This automatically handles gzip, deflate and brotli encoded responses. - """ - for chunk in self.http_response.iter_bytes(chunk_size): - yield chunk - - def iter_text(self, chunk_size: int | None = None) -> Iterator[str]: - """A str-iterator over the decoded response content - that handles both gzip, deflate, etc but also detects the content's - string encoding. - """ - for chunk in self.http_response.iter_text(chunk_size): - yield chunk - - def iter_lines(self) -> Iterator[str]: - """Like `iter_text()` but will only yield chunks for each line""" - for chunk in self.http_response.iter_lines(): - yield chunk - - -class AsyncAPIResponse(BaseAPIResponse[R]): - @overload - async def parse(self, *, to: type[_T]) -> _T: ... - - @overload - async def parse(self) -> R: ... - - async def parse(self, *, to: type[_T] | None = None) -> R | _T: - """Returns the rich python representation of this response's data. - - For lower-level control, see `.read()`, `.json()`, `.iter_bytes()`. - - You can customise the type that the response is parsed into through - the `to` argument, e.g. - - ```py - from supermemory import BaseModel - - - class MyModel(BaseModel): - foo: str - - - obj = response.parse(to=MyModel) - print(obj.foo) - ``` - - We support parsing: - - `BaseModel` - - `dict` - - `list` - - `Union` - - `str` - - `httpx.Response` - """ - cache_key = to if to is not None else self._cast_to - cached = self._parsed_by_type.get(cache_key) - if cached is not None: - return cached # type: ignore[no-any-return] - - if not self._is_sse_stream: - await self.read() - - parsed = self._parse(to=to) - if is_given(self._options.post_parser): - parsed = self._options.post_parser(parsed) - - self._parsed_by_type[cache_key] = parsed - return parsed - - async def read(self) -> bytes: - """Read and return the binary response content.""" - try: - return await self.http_response.aread() - except httpx.StreamConsumed as exc: - # the default error raised by httpx isn't very - # helpful in our case so we re-raise it with - # a different error message - raise StreamAlreadyConsumed() from exc - - async def text(self) -> str: - """Read and decode the response content into a string.""" - await self.read() - return self.http_response.text - - async def json(self) -> object: - """Read and decode the JSON response content.""" - await self.read() - return self.http_response.json() - - async def close(self) -> None: - """Close the response and release the connection. - - Automatically called if the response body is read to completion. - """ - await self.http_response.aclose() - - async def iter_bytes(self, chunk_size: int | None = None) -> AsyncIterator[bytes]: - """ - A byte-iterator over the decoded response content. - - This automatically handles gzip, deflate and brotli encoded responses. - """ - async for chunk in self.http_response.aiter_bytes(chunk_size): - yield chunk - - async def iter_text(self, chunk_size: int | None = None) -> AsyncIterator[str]: - """A str-iterator over the decoded response content - that handles both gzip, deflate, etc but also detects the content's - string encoding. - """ - async for chunk in self.http_response.aiter_text(chunk_size): - yield chunk - - async def iter_lines(self) -> AsyncIterator[str]: - """Like `iter_text()` but will only yield chunks for each line""" - async for chunk in self.http_response.aiter_lines(): - yield chunk - - -class BinaryAPIResponse(APIResponse[bytes]): - """Subclass of APIResponse providing helpers for dealing with binary data. - - Note: If you want to stream the response data instead of eagerly reading it - all at once then you should use `.with_streaming_response` when making - the API request, e.g. `.with_streaming_response.get_binary_response()` - """ - - def write_to_file( - self, - file: str | os.PathLike[str], - ) -> None: - """Write the output to the given file. - - Accepts a filename or any path-like object, e.g. pathlib.Path - - Note: if you want to stream the data to the file instead of writing - all at once then you should use `.with_streaming_response` when making - the API request, e.g. `.with_streaming_response.get_binary_response()` - """ - with open(file, mode="wb") as f: - for data in self.iter_bytes(): - f.write(data) - - -class AsyncBinaryAPIResponse(AsyncAPIResponse[bytes]): - """Subclass of APIResponse providing helpers for dealing with binary data. - - Note: If you want to stream the response data instead of eagerly reading it - all at once then you should use `.with_streaming_response` when making - the API request, e.g. `.with_streaming_response.get_binary_response()` - """ - - async def write_to_file( - self, - file: str | os.PathLike[str], - ) -> None: - """Write the output to the given file. - - Accepts a filename or any path-like object, e.g. pathlib.Path - - Note: if you want to stream the data to the file instead of writing - all at once then you should use `.with_streaming_response` when making - the API request, e.g. `.with_streaming_response.get_binary_response()` - """ - path = anyio.Path(file) - async with await path.open(mode="wb") as f: - async for data in self.iter_bytes(): - await f.write(data) - - -class StreamedBinaryAPIResponse(APIResponse[bytes]): - def stream_to_file( - self, - file: str | os.PathLike[str], - *, - chunk_size: int | None = None, - ) -> None: - """Streams the output to the given file. - - Accepts a filename or any path-like object, e.g. pathlib.Path - """ - with open(file, mode="wb") as f: - for data in self.iter_bytes(chunk_size): - f.write(data) - - -class AsyncStreamedBinaryAPIResponse(AsyncAPIResponse[bytes]): - async def stream_to_file( - self, - file: str | os.PathLike[str], - *, - chunk_size: int | None = None, - ) -> None: - """Streams the output to the given file. - - Accepts a filename or any path-like object, e.g. pathlib.Path - """ - path = anyio.Path(file) - async with await path.open(mode="wb") as f: - async for data in self.iter_bytes(chunk_size): - await f.write(data) - - -class MissingStreamClassError(TypeError): - def __init__(self) -> None: - super().__init__( - "The `stream` argument was set to `True` but the `stream_cls` argument was not given. See `supermemory._streaming` for reference", - ) - - -class StreamAlreadyConsumed(SupermemoryError): - """ - Attempted to read or stream content, but the content has already - been streamed. - - This can happen if you use a method like `.iter_lines()` and then attempt - to read th entire response body afterwards, e.g. - - ```py - response = await client.post(...) - async for line in response.iter_lines(): - ... # do something with `line` - - content = await response.read() - # ^ error - ``` - - If you want this behaviour you'll need to either manually accumulate the response - content or call `await response.read()` before iterating over the stream. - """ - - def __init__(self) -> None: - message = ( - "Attempted to read or stream some content, but the content has " - "already been streamed. " - "This could be due to attempting to stream the response " - "content more than once." - "\n\n" - "You can fix this by manually accumulating the response content while streaming " - "or by calling `.read()` before starting to stream." - ) - super().__init__(message) - - -class ResponseContextManager(Generic[_APIResponseT]): - """Context manager for ensuring that a request is not made - until it is entered and that the response will always be closed - when the context manager exits - """ - - def __init__(self, request_func: Callable[[], _APIResponseT]) -> None: - self._request_func = request_func - self.__response: _APIResponseT | None = None - - def __enter__(self) -> _APIResponseT: - self.__response = self._request_func() - return self.__response - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - if self.__response is not None: - self.__response.close() - - -class AsyncResponseContextManager(Generic[_AsyncAPIResponseT]): - """Context manager for ensuring that a request is not made - until it is entered and that the response will always be closed - when the context manager exits - """ - - def __init__(self, api_request: Awaitable[_AsyncAPIResponseT]) -> None: - self._api_request = api_request - self.__response: _AsyncAPIResponseT | None = None - - async def __aenter__(self) -> _AsyncAPIResponseT: - self.__response = await self._api_request - return self.__response - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - if self.__response is not None: - await self.__response.close() - - -def to_streamed_response_wrapper(func: Callable[P, R]) -> Callable[P, ResponseContextManager[APIResponse[R]]]: - """Higher order function that takes one of our bound API methods and wraps it - to support streaming and returning the raw `APIResponse` object directly. - """ - - @functools.wraps(func) - def wrapped(*args: P.args, **kwargs: P.kwargs) -> ResponseContextManager[APIResponse[R]]: - extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} - extra_headers[RAW_RESPONSE_HEADER] = "stream" - - kwargs["extra_headers"] = extra_headers - - make_request = functools.partial(func, *args, **kwargs) - - return ResponseContextManager(cast(Callable[[], APIResponse[R]], make_request)) - - return wrapped - - -def async_to_streamed_response_wrapper( - func: Callable[P, Awaitable[R]], -) -> Callable[P, AsyncResponseContextManager[AsyncAPIResponse[R]]]: - """Higher order function that takes one of our bound API methods and wraps it - to support streaming and returning the raw `APIResponse` object directly. - """ - - @functools.wraps(func) - def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncResponseContextManager[AsyncAPIResponse[R]]: - extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} - extra_headers[RAW_RESPONSE_HEADER] = "stream" - - kwargs["extra_headers"] = extra_headers - - make_request = func(*args, **kwargs) - - return AsyncResponseContextManager(cast(Awaitable[AsyncAPIResponse[R]], make_request)) - - return wrapped - - -def to_custom_streamed_response_wrapper( - func: Callable[P, object], - response_cls: type[_APIResponseT], -) -> Callable[P, ResponseContextManager[_APIResponseT]]: - """Higher order function that takes one of our bound API methods and an `APIResponse` class - and wraps the method to support streaming and returning the given response class directly. - - Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` - """ - - @functools.wraps(func) - def wrapped(*args: P.args, **kwargs: P.kwargs) -> ResponseContextManager[_APIResponseT]: - extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} - extra_headers[RAW_RESPONSE_HEADER] = "stream" - extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls - - kwargs["extra_headers"] = extra_headers - - make_request = functools.partial(func, *args, **kwargs) - - return ResponseContextManager(cast(Callable[[], _APIResponseT], make_request)) - - return wrapped - - -def async_to_custom_streamed_response_wrapper( - func: Callable[P, Awaitable[object]], - response_cls: type[_AsyncAPIResponseT], -) -> Callable[P, AsyncResponseContextManager[_AsyncAPIResponseT]]: - """Higher order function that takes one of our bound API methods and an `APIResponse` class - and wraps the method to support streaming and returning the given response class directly. - - Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` - """ - - @functools.wraps(func) - def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncResponseContextManager[_AsyncAPIResponseT]: - extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} - extra_headers[RAW_RESPONSE_HEADER] = "stream" - extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls - - kwargs["extra_headers"] = extra_headers - - make_request = func(*args, **kwargs) - - return AsyncResponseContextManager(cast(Awaitable[_AsyncAPIResponseT], make_request)) - - return wrapped - - -def to_raw_response_wrapper(func: Callable[P, R]) -> Callable[P, APIResponse[R]]: - """Higher order function that takes one of our bound API methods and wraps it - to support returning the raw `APIResponse` object directly. - """ - - @functools.wraps(func) - def wrapped(*args: P.args, **kwargs: P.kwargs) -> APIResponse[R]: - extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} - extra_headers[RAW_RESPONSE_HEADER] = "raw" - - kwargs["extra_headers"] = extra_headers - - return cast(APIResponse[R], func(*args, **kwargs)) - - return wrapped - - -def async_to_raw_response_wrapper(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[AsyncAPIResponse[R]]]: - """Higher order function that takes one of our bound API methods and wraps it - to support returning the raw `APIResponse` object directly. - """ - - @functools.wraps(func) - async def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncAPIResponse[R]: - extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} - extra_headers[RAW_RESPONSE_HEADER] = "raw" - - kwargs["extra_headers"] = extra_headers - - return cast(AsyncAPIResponse[R], await func(*args, **kwargs)) - - return wrapped - - -def to_custom_raw_response_wrapper( - func: Callable[P, object], - response_cls: type[_APIResponseT], -) -> Callable[P, _APIResponseT]: - """Higher order function that takes one of our bound API methods and an `APIResponse` class - and wraps the method to support returning the given response class directly. - - Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` - """ - - @functools.wraps(func) - def wrapped(*args: P.args, **kwargs: P.kwargs) -> _APIResponseT: - extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} - extra_headers[RAW_RESPONSE_HEADER] = "raw" - extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls - - kwargs["extra_headers"] = extra_headers - - return cast(_APIResponseT, func(*args, **kwargs)) - - return wrapped - - -def async_to_custom_raw_response_wrapper( - func: Callable[P, Awaitable[object]], - response_cls: type[_AsyncAPIResponseT], -) -> Callable[P, Awaitable[_AsyncAPIResponseT]]: - """Higher order function that takes one of our bound API methods and an `APIResponse` class - and wraps the method to support returning the given response class directly. - - Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` - """ - - @functools.wraps(func) - def wrapped(*args: P.args, **kwargs: P.kwargs) -> Awaitable[_AsyncAPIResponseT]: - extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} - extra_headers[RAW_RESPONSE_HEADER] = "raw" - extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls - - kwargs["extra_headers"] = extra_headers - - return cast(Awaitable[_AsyncAPIResponseT], func(*args, **kwargs)) - - return wrapped - - -def extract_response_type(typ: type[BaseAPIResponse[Any]]) -> type: - """Given a type like `APIResponse[T]`, returns the generic type variable `T`. - - This also handles the case where a concrete subclass is given, e.g. - ```py - class MyResponse(APIResponse[bytes]): - ... - - extract_response_type(MyResponse) -> bytes - ``` - """ - return extract_type_var_from_base( - typ, - generic_bases=cast("tuple[type, ...]", (BaseAPIResponse, APIResponse, AsyncAPIResponse)), - index=0, - ) diff --git a/src/supermemory/_streaming.py b/src/supermemory/_streaming.py deleted file mode 100644 index 9c2ab2d8..00000000 --- a/src/supermemory/_streaming.py +++ /dev/null @@ -1,338 +0,0 @@ -# Note: initially copied from https://github.com/florimondmanca/httpx-sse/blob/master/src/httpx_sse/_decoders.py -from __future__ import annotations - -import json -import inspect -from types import TracebackType -from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, Optional, AsyncIterator, cast -from typing_extensions import Self, Protocol, TypeGuard, override, get_origin, runtime_checkable - -import httpx - -from ._utils import extract_type_var_from_base - -if TYPE_CHECKING: - from ._client import Supermemory, AsyncSupermemory - from ._models import FinalRequestOptions - - -_T = TypeVar("_T") - - -class Stream(Generic[_T]): - """Provides the core interface to iterate over a synchronous stream response.""" - - response: httpx.Response - _options: Optional[FinalRequestOptions] = None - _decoder: SSEBytesDecoder - - def __init__( - self, - *, - cast_to: type[_T], - response: httpx.Response, - client: Supermemory, - options: Optional[FinalRequestOptions] = None, - ) -> None: - self.response = response - self._cast_to = cast_to - self._client = client - self._options = options - self._decoder = client._make_sse_decoder() - self._iterator = self.__stream__() - - def __next__(self) -> _T: - return self._iterator.__next__() - - def __iter__(self) -> Iterator[_T]: - for item in self._iterator: - yield item - - def _iter_events(self) -> Iterator[ServerSentEvent]: - yield from self._decoder.iter_bytes(self.response.iter_bytes()) - - def __stream__(self) -> Iterator[_T]: - cast_to = cast(Any, self._cast_to) - response = self.response - process_data = self._client._process_response_data - iterator = self._iter_events() - - try: - for sse in iterator: - yield process_data(data=sse.json(), cast_to=cast_to, response=response) - finally: - # Ensure the response is closed even if the consumer doesn't read all data - response.close() - - def __enter__(self) -> Self: - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - self.close() - - def close(self) -> None: - """ - Close the response and release the connection. - - Automatically called if the response body is read to completion. - """ - self.response.close() - - -class AsyncStream(Generic[_T]): - """Provides the core interface to iterate over an asynchronous stream response.""" - - response: httpx.Response - _options: Optional[FinalRequestOptions] = None - _decoder: SSEDecoder | SSEBytesDecoder - - def __init__( - self, - *, - cast_to: type[_T], - response: httpx.Response, - client: AsyncSupermemory, - options: Optional[FinalRequestOptions] = None, - ) -> None: - self.response = response - self._cast_to = cast_to - self._client = client - self._options = options - self._decoder = client._make_sse_decoder() - self._iterator = self.__stream__() - - async def __anext__(self) -> _T: - return await self._iterator.__anext__() - - async def __aiter__(self) -> AsyncIterator[_T]: - async for item in self._iterator: - yield item - - async def _iter_events(self) -> AsyncIterator[ServerSentEvent]: - async for sse in self._decoder.aiter_bytes(self.response.aiter_bytes()): - yield sse - - async def __stream__(self) -> AsyncIterator[_T]: - cast_to = cast(Any, self._cast_to) - response = self.response - process_data = self._client._process_response_data - iterator = self._iter_events() - - try: - async for sse in iterator: - yield process_data(data=sse.json(), cast_to=cast_to, response=response) - finally: - # Ensure the response is closed even if the consumer doesn't read all data - await response.aclose() - - async def __aenter__(self) -> Self: - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - await self.close() - - async def close(self) -> None: - """ - Close the response and release the connection. - - Automatically called if the response body is read to completion. - """ - await self.response.aclose() - - -class ServerSentEvent: - def __init__( - self, - *, - event: str | None = None, - data: str | None = None, - id: str | None = None, - retry: int | None = None, - ) -> None: - if data is None: - data = "" - - self._id = id - self._data = data - self._event = event or None - self._retry = retry - - @property - def event(self) -> str | None: - return self._event - - @property - def id(self) -> str | None: - return self._id - - @property - def retry(self) -> int | None: - return self._retry - - @property - def data(self) -> str: - return self._data - - def json(self) -> Any: - return json.loads(self.data) - - @override - def __repr__(self) -> str: - return f"ServerSentEvent(event={self.event}, data={self.data}, id={self.id}, retry={self.retry})" - - -class SSEDecoder: - _data: list[str] - _event: str | None - _retry: int | None - _last_event_id: str | None - - def __init__(self) -> None: - self._event = None - self._data = [] - self._last_event_id = None - self._retry = None - - def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]: - """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" - for chunk in self._iter_chunks(iterator): - # Split before decoding so splitlines() only uses \r and \n - for raw_line in chunk.splitlines(): - line = raw_line.decode("utf-8") - sse = self.decode(line) - if sse: - yield sse - - def _iter_chunks(self, iterator: Iterator[bytes]) -> Iterator[bytes]: - """Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks""" - data = b"" - for chunk in iterator: - for line in chunk.splitlines(keepends=True): - data += line - if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")): - yield data - data = b"" - if data: - yield data - - async def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]: - """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" - async for chunk in self._aiter_chunks(iterator): - # Split before decoding so splitlines() only uses \r and \n - for raw_line in chunk.splitlines(): - line = raw_line.decode("utf-8") - sse = self.decode(line) - if sse: - yield sse - - async def _aiter_chunks(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[bytes]: - """Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks""" - data = b"" - async for chunk in iterator: - for line in chunk.splitlines(keepends=True): - data += line - if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")): - yield data - data = b"" - if data: - yield data - - def decode(self, line: str) -> ServerSentEvent | None: - # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 - - if not line: - if not self._event and not self._data and not self._last_event_id and self._retry is None: - return None - - sse = ServerSentEvent( - event=self._event, - data="\n".join(self._data), - id=self._last_event_id, - retry=self._retry, - ) - - # NOTE: as per the SSE spec, do not reset last_event_id. - self._event = None - self._data = [] - self._retry = None - - return sse - - if line.startswith(":"): - return None - - fieldname, _, value = line.partition(":") - - if value.startswith(" "): - value = value[1:] - - if fieldname == "event": - self._event = value - elif fieldname == "data": - self._data.append(value) - elif fieldname == "id": - if "\0" in value: - pass - else: - self._last_event_id = value - elif fieldname == "retry": - try: - self._retry = int(value) - except (TypeError, ValueError): - pass - else: - pass # Field is ignored. - - return None - - -@runtime_checkable -class SSEBytesDecoder(Protocol): - def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]: - """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" - ... - - def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]: - """Given an async iterator that yields raw binary data, iterate over it & yield every event encountered""" - ... - - -def is_stream_class_type(typ: type) -> TypeGuard[type[Stream[object]] | type[AsyncStream[object]]]: - """TypeGuard for determining whether or not the given type is a subclass of `Stream` / `AsyncStream`""" - origin = get_origin(typ) or typ - return inspect.isclass(origin) and issubclass(origin, (Stream, AsyncStream)) - - -def extract_stream_chunk_type( - stream_cls: type, - *, - failure_message: str | None = None, -) -> type: - """Given a type like `Stream[T]`, returns the generic type variable `T`. - - This also handles the case where a concrete subclass is given, e.g. - ```py - class MyStream(Stream[bytes]): - ... - - extract_stream_chunk_type(MyStream) -> bytes - ``` - """ - from ._base_client import Stream, AsyncStream - - return extract_type_var_from_base( - stream_cls, - index=0, - generic_bases=cast("tuple[type, ...]", (Stream, AsyncStream)), - failure_message=failure_message, - ) diff --git a/src/supermemory/_types.py b/src/supermemory/_types.py deleted file mode 100644 index 92dab54b..00000000 --- a/src/supermemory/_types.py +++ /dev/null @@ -1,273 +0,0 @@ -from __future__ import annotations - -from os import PathLike -from typing import ( - IO, - TYPE_CHECKING, - Any, - Dict, - List, - Type, - Tuple, - Union, - Mapping, - TypeVar, - Callable, - Iterable, - Iterator, - Optional, - Sequence, - AsyncIterable, -) -from typing_extensions import ( - Set, - Literal, - Protocol, - TypeAlias, - TypedDict, - SupportsIndex, - overload, - override, - runtime_checkable, -) - -import httpx -import pydantic -from httpx import URL, Proxy, Timeout, Response, BaseTransport, AsyncBaseTransport - -if TYPE_CHECKING: - from ._models import BaseModel - from ._response import APIResponse, AsyncAPIResponse - -Transport = BaseTransport -AsyncTransport = AsyncBaseTransport -Query = Mapping[str, object] -Body = object -AnyMapping = Mapping[str, object] -ModelT = TypeVar("ModelT", bound=pydantic.BaseModel) -_T = TypeVar("_T") - -ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] -NestedFormat = Literal["dots", "brackets"] - - -# Approximates httpx internal ProxiesTypes and RequestFiles types -# while adding support for `PathLike` instances -ProxiesDict = Dict["str | URL", Union[None, str, URL, Proxy]] -ProxiesTypes = Union[str, Proxy, ProxiesDict] -if TYPE_CHECKING: - Base64FileInput = Union[IO[bytes], PathLike[str]] - FileContent = Union[IO[bytes], bytes, PathLike[str]] -else: - Base64FileInput = Union[IO[bytes], PathLike] - FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. - - -# Used for sending raw binary data / streaming data in request bodies -# e.g. for file uploads without multipart encoding -BinaryTypes = Union[bytes, bytearray, IO[bytes], Iterable[bytes]] -AsyncBinaryTypes = Union[bytes, bytearray, IO[bytes], AsyncIterable[bytes]] - -FileTypes = Union[ - # file (or bytes) - FileContent, - # (filename, file (or bytes)) - Tuple[Optional[str], FileContent], - # (filename, file (or bytes), content_type) - Tuple[Optional[str], FileContent, Optional[str]], - # (filename, file (or bytes), content_type, headers) - Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], -] -RequestFiles = Union[Mapping[str, FileTypes], Sequence[Tuple[str, FileTypes]]] - -# duplicate of the above but without our custom file support -HttpxFileContent = Union[IO[bytes], bytes] -HttpxFileTypes = Union[ - # file (or bytes) - HttpxFileContent, - # (filename, file (or bytes)) - Tuple[Optional[str], HttpxFileContent], - # (filename, file (or bytes), content_type) - Tuple[Optional[str], HttpxFileContent, Optional[str]], - # (filename, file (or bytes), content_type, headers) - Tuple[Optional[str], HttpxFileContent, Optional[str], Mapping[str, str]], -] -HttpxRequestFiles = Union[Mapping[str, HttpxFileTypes], Sequence[Tuple[str, HttpxFileTypes]]] - -# Workaround to support (cast_to: Type[ResponseT]) -> ResponseT -# where ResponseT includes `None`. In order to support directly -# passing `None`, overloads would have to be defined for every -# method that uses `ResponseT` which would lead to an unacceptable -# amount of code duplication and make it unreadable. See _base_client.py -# for example usage. -# -# This unfortunately means that you will either have -# to import this type and pass it explicitly: -# -# from supermemory import NoneType -# client.get('/foo', cast_to=NoneType) -# -# or build it yourself: -# -# client.get('/foo', cast_to=type(None)) -if TYPE_CHECKING: - NoneType: Type[None] -else: - NoneType = type(None) - - -class RequestOptions(TypedDict, total=False): - headers: Headers - max_retries: int - timeout: float | Timeout | None - params: Query - extra_json: AnyMapping - idempotency_key: str - follow_redirects: bool - - -# Sentinel class used until PEP 0661 is accepted -class NotGiven: - """ - For parameters with a meaningful None value, we need to distinguish between - the user explicitly passing None, and the user not passing the parameter at - all. - - User code shouldn't need to use not_given directly. - - For example: - - ```py - def create(timeout: Timeout | None | NotGiven = not_given): ... - - - create(timeout=1) # 1s timeout - create(timeout=None) # No timeout - create() # Default timeout behavior - ``` - """ - - def __bool__(self) -> Literal[False]: - return False - - @override - def __repr__(self) -> str: - return "NOT_GIVEN" - - -not_given = NotGiven() -# for backwards compatibility: -NOT_GIVEN = NotGiven() - - -class Omit: - """ - To explicitly omit something from being sent in a request, use `omit`. - - ```py - # as the default `Content-Type` header is `application/json` that will be sent - client.post("/upload/files", files={"file": b"my raw file content"}) - - # you can't explicitly override the header as it has to be dynamically generated - # to look something like: 'multipart/form-data; boundary=0d8382fcf5f8c3be01ca2e11002d2983' - client.post(..., headers={"Content-Type": "multipart/form-data"}) - - # instead you can remove the default `application/json` header by passing omit - client.post(..., headers={"Content-Type": omit}) - ``` - """ - - def __bool__(self) -> Literal[False]: - return False - - -omit = Omit() - - -@runtime_checkable -class ModelBuilderProtocol(Protocol): - @classmethod - def build( - cls: type[_T], - *, - response: Response, - data: object, - ) -> _T: ... - - -Headers = Mapping[str, Union[str, Omit]] - - -class HeadersLikeProtocol(Protocol): - def get(self, __key: str) -> str | None: ... - - -HeadersLike = Union[Headers, HeadersLikeProtocol] - -ResponseT = TypeVar( - "ResponseT", - bound=Union[ - object, - str, - None, - "BaseModel", - List[Any], - Dict[str, Any], - Response, - ModelBuilderProtocol, - "APIResponse[Any]", - "AsyncAPIResponse[Any]", - ], -) - -StrBytesIntFloat = Union[str, bytes, int, float] - -# Note: copied from Pydantic -# https://github.com/pydantic/pydantic/blob/6f31f8f68ef011f84357330186f603ff295312fd/pydantic/main.py#L79 -IncEx: TypeAlias = Union[Set[int], Set[str], Mapping[int, Union["IncEx", bool]], Mapping[str, Union["IncEx", bool]]] - -PostParser = Callable[[Any], Any] - - -@runtime_checkable -class InheritsGeneric(Protocol): - """Represents a type that has inherited from `Generic` - - The `__orig_bases__` property can be used to determine the resolved - type variable for a given base class. - """ - - __orig_bases__: tuple[_GenericAlias] - - -class _GenericAlias(Protocol): - __origin__: type[object] - - -class HttpxSendArgs(TypedDict, total=False): - auth: httpx.Auth - follow_redirects: bool - - -_T_co = TypeVar("_T_co", covariant=True) - - -if TYPE_CHECKING: - # This works because str.__contains__ does not accept object (either in typeshed or at runtime) - # https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285 - # - # Note: index() and count() methods are intentionally omitted to allow pyright to properly - # infer TypedDict types when dict literals are used in lists assigned to SequenceNotStr. - class SequenceNotStr(Protocol[_T_co]): - @overload - def __getitem__(self, index: SupportsIndex, /) -> _T_co: ... - @overload - def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ... - def __contains__(self, value: object, /) -> bool: ... - def __len__(self) -> int: ... - def __iter__(self) -> Iterator[_T_co]: ... - def __reversed__(self) -> Iterator[_T_co]: ... -else: - # just point this to a normal `Sequence` at runtime to avoid having to special case - # deserializing our custom sequence type - SequenceNotStr = Sequence diff --git a/src/supermemory/_utils/__init__.py b/src/supermemory/_utils/__init__.py deleted file mode 100644 index 1c090e51..00000000 --- a/src/supermemory/_utils/__init__.py +++ /dev/null @@ -1,64 +0,0 @@ -from ._path import path_template as path_template -from ._sync import asyncify as asyncify -from ._proxy import LazyProxy as LazyProxy -from ._utils import ( - flatten as flatten, - is_dict as is_dict, - is_list as is_list, - is_given as is_given, - is_tuple as is_tuple, - json_safe as json_safe, - lru_cache as lru_cache, - is_mapping as is_mapping, - is_tuple_t as is_tuple_t, - is_iterable as is_iterable, - is_sequence as is_sequence, - coerce_float as coerce_float, - is_mapping_t as is_mapping_t, - removeprefix as removeprefix, - removesuffix as removesuffix, - extract_files as extract_files, - is_sequence_t as is_sequence_t, - required_args as required_args, - coerce_boolean as coerce_boolean, - coerce_integer as coerce_integer, - file_from_path as file_from_path, - strip_not_given as strip_not_given, - get_async_library as get_async_library, - maybe_coerce_float as maybe_coerce_float, - get_required_header as get_required_header, - maybe_coerce_boolean as maybe_coerce_boolean, - maybe_coerce_integer as maybe_coerce_integer, -) -from ._compat import ( - get_args as get_args, - is_union as is_union, - get_origin as get_origin, - is_typeddict as is_typeddict, - is_literal_type as is_literal_type, -) -from ._typing import ( - is_list_type as is_list_type, - is_union_type as is_union_type, - extract_type_arg as extract_type_arg, - is_iterable_type as is_iterable_type, - is_required_type as is_required_type, - is_sequence_type as is_sequence_type, - is_annotated_type as is_annotated_type, - is_type_alias_type as is_type_alias_type, - strip_annotated_type as strip_annotated_type, - extract_type_var_from_base as extract_type_var_from_base, -) -from ._streams import consume_sync_iterator as consume_sync_iterator, consume_async_iterator as consume_async_iterator -from ._transform import ( - PropertyInfo as PropertyInfo, - transform as transform, - async_transform as async_transform, - maybe_transform as maybe_transform, - async_maybe_transform as async_maybe_transform, -) -from ._reflection import ( - function_has_argument as function_has_argument, - assert_signatures_in_sync as assert_signatures_in_sync, -) -from ._datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime diff --git a/src/supermemory/_utils/_compat.py b/src/supermemory/_utils/_compat.py deleted file mode 100644 index 2c70b299..00000000 --- a/src/supermemory/_utils/_compat.py +++ /dev/null @@ -1,45 +0,0 @@ -from __future__ import annotations - -import sys -import typing_extensions -from typing import Any, Type, Union, Literal, Optional -from datetime import date, datetime -from typing_extensions import get_args as _get_args, get_origin as _get_origin - -from .._types import StrBytesIntFloat -from ._datetime_parse import parse_date as _parse_date, parse_datetime as _parse_datetime - -_LITERAL_TYPES = {Literal, typing_extensions.Literal} - - -def get_args(tp: type[Any]) -> tuple[Any, ...]: - return _get_args(tp) - - -def get_origin(tp: type[Any]) -> type[Any] | None: - return _get_origin(tp) - - -def is_union(tp: Optional[Type[Any]]) -> bool: - if sys.version_info < (3, 10): - return tp is Union # type: ignore[comparison-overlap] - else: - import types - - return tp is Union or tp is types.UnionType # type: ignore[comparison-overlap] - - -def is_typeddict(tp: Type[Any]) -> bool: - return typing_extensions.is_typeddict(tp) - - -def is_literal_type(tp: Type[Any]) -> bool: - return get_origin(tp) in _LITERAL_TYPES - - -def parse_date(value: Union[date, StrBytesIntFloat]) -> date: - return _parse_date(value) - - -def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: - return _parse_datetime(value) diff --git a/src/supermemory/_utils/_datetime_parse.py b/src/supermemory/_utils/_datetime_parse.py deleted file mode 100644 index 7cb9d9e6..00000000 --- a/src/supermemory/_utils/_datetime_parse.py +++ /dev/null @@ -1,136 +0,0 @@ -""" -This file contains code from https://github.com/pydantic/pydantic/blob/main/pydantic/v1/datetime_parse.py -without the Pydantic v1 specific errors. -""" - -from __future__ import annotations - -import re -from typing import Dict, Union, Optional -from datetime import date, datetime, timezone, timedelta - -from .._types import StrBytesIntFloat - -date_expr = r"(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})" -time_expr = ( - r"(?P\d{1,2}):(?P\d{1,2})" - r"(?::(?P\d{1,2})(?:\.(?P\d{1,6})\d{0,6})?)?" - r"(?PZ|[+-]\d{2}(?::?\d{2})?)?$" -) - -date_re = re.compile(f"{date_expr}$") -datetime_re = re.compile(f"{date_expr}[T ]{time_expr}") - - -EPOCH = datetime(1970, 1, 1) -# if greater than this, the number is in ms, if less than or equal it's in seconds -# (in seconds this is 11th October 2603, in ms it's 20th August 1970) -MS_WATERSHED = int(2e10) -# slightly more than datetime.max in ns - (datetime.max - EPOCH).total_seconds() * 1e9 -MAX_NUMBER = int(3e20) - - -def _get_numeric(value: StrBytesIntFloat, native_expected_type: str) -> Union[None, int, float]: - if isinstance(value, (int, float)): - return value - try: - return float(value) - except ValueError: - return None - except TypeError: - raise TypeError(f"invalid type; expected {native_expected_type}, string, bytes, int or float") from None - - -def _from_unix_seconds(seconds: Union[int, float]) -> datetime: - if seconds > MAX_NUMBER: - return datetime.max - elif seconds < -MAX_NUMBER: - return datetime.min - - while abs(seconds) > MS_WATERSHED: - seconds /= 1000 - dt = EPOCH + timedelta(seconds=seconds) - return dt.replace(tzinfo=timezone.utc) - - -def _parse_timezone(value: Optional[str]) -> Union[None, int, timezone]: - if value == "Z": - return timezone.utc - elif value is not None: - offset_mins = int(value[-2:]) if len(value) > 3 else 0 - offset = 60 * int(value[1:3]) + offset_mins - if value[0] == "-": - offset = -offset - return timezone(timedelta(minutes=offset)) - else: - return None - - -def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: - """ - Parse a datetime/int/float/string and return a datetime.datetime. - - This function supports time zone offsets. When the input contains one, - the output uses a timezone with a fixed offset from UTC. - - Raise ValueError if the input is well formatted but not a valid datetime. - Raise ValueError if the input isn't well formatted. - """ - if isinstance(value, datetime): - return value - - number = _get_numeric(value, "datetime") - if number is not None: - return _from_unix_seconds(number) - - if isinstance(value, bytes): - value = value.decode() - - assert not isinstance(value, (float, int)) - - match = datetime_re.match(value) - if match is None: - raise ValueError("invalid datetime format") - - kw = match.groupdict() - if kw["microsecond"]: - kw["microsecond"] = kw["microsecond"].ljust(6, "0") - - tzinfo = _parse_timezone(kw.pop("tzinfo")) - kw_: Dict[str, Union[None, int, timezone]] = {k: int(v) for k, v in kw.items() if v is not None} - kw_["tzinfo"] = tzinfo - - return datetime(**kw_) # type: ignore - - -def parse_date(value: Union[date, StrBytesIntFloat]) -> date: - """ - Parse a date/int/float/string and return a datetime.date. - - Raise ValueError if the input is well formatted but not a valid date. - Raise ValueError if the input isn't well formatted. - """ - if isinstance(value, date): - if isinstance(value, datetime): - return value.date() - else: - return value - - number = _get_numeric(value, "date") - if number is not None: - return _from_unix_seconds(number).date() - - if isinstance(value, bytes): - value = value.decode() - - assert not isinstance(value, (float, int)) - match = date_re.match(value) - if match is None: - raise ValueError("invalid date format") - - kw = {k: int(v) for k, v in match.groupdict().items()} - - try: - return date(**kw) - except ValueError: - raise ValueError("invalid date format") from None diff --git a/src/supermemory/_utils/_json.py b/src/supermemory/_utils/_json.py deleted file mode 100644 index 60584214..00000000 --- a/src/supermemory/_utils/_json.py +++ /dev/null @@ -1,35 +0,0 @@ -import json -from typing import Any -from datetime import datetime -from typing_extensions import override - -import pydantic - -from .._compat import model_dump - - -def openapi_dumps(obj: Any) -> bytes: - """ - Serialize an object to UTF-8 encoded JSON bytes. - - Extends the standard json.dumps with support for additional types - commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc. - """ - return json.dumps( - obj, - cls=_CustomEncoder, - # Uses the same defaults as httpx's JSON serialization - ensure_ascii=False, - separators=(",", ":"), - allow_nan=False, - ).encode() - - -class _CustomEncoder(json.JSONEncoder): - @override - def default(self, o: Any) -> Any: - if isinstance(o, datetime): - return o.isoformat() - if isinstance(o, pydantic.BaseModel): - return model_dump(o, exclude_unset=True, mode="json", by_alias=True) - return super().default(o) diff --git a/src/supermemory/_utils/_logs.py b/src/supermemory/_utils/_logs.py deleted file mode 100644 index b4d8cbf3..00000000 --- a/src/supermemory/_utils/_logs.py +++ /dev/null @@ -1,25 +0,0 @@ -import os -import logging - -logger: logging.Logger = logging.getLogger("supermemory") -httpx_logger: logging.Logger = logging.getLogger("httpx") - - -def _basic_config() -> None: - # e.g. [2023-10-05 14:12:26 - supermemory._base_client:818 - DEBUG] HTTP Request: POST http://127.0.0.1:4010/foo/bar "200 OK" - logging.basicConfig( - format="[%(asctime)s - %(name)s:%(lineno)d - %(levelname)s] %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", - ) - - -def setup_logging() -> None: - env = os.environ.get("SUPERMEMORY_LOG") - if env == "debug": - _basic_config() - logger.setLevel(logging.DEBUG) - httpx_logger.setLevel(logging.DEBUG) - elif env == "info": - _basic_config() - logger.setLevel(logging.INFO) - httpx_logger.setLevel(logging.INFO) diff --git a/src/supermemory/_utils/_path.py b/src/supermemory/_utils/_path.py deleted file mode 100644 index 4d6e1e4c..00000000 --- a/src/supermemory/_utils/_path.py +++ /dev/null @@ -1,127 +0,0 @@ -from __future__ import annotations - -import re -from typing import ( - Any, - Mapping, - Callable, -) -from urllib.parse import quote - -# Matches '.' or '..' where each dot is either literal or percent-encoded (%2e / %2E). -_DOT_SEGMENT_RE = re.compile(r"^(?:\.|%2[eE]){1,2}$") - -_PLACEHOLDER_RE = re.compile(r"\{(\w+)\}") - - -def _quote_path_segment_part(value: str) -> str: - """Percent-encode `value` for use in a URI path segment. - - Considers characters not in `pchar` set from RFC 3986 §3.3 to be unsafe. - https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 - """ - # quote() already treats unreserved characters (letters, digits, and -._~) - # as safe, so we only need to add sub-delims, ':', and '@'. - # Notably, unlike the default `safe` for quote(), / is unsafe and must be quoted. - return quote(value, safe="!$&'()*+,;=:@") - - -def _quote_query_part(value: str) -> str: - """Percent-encode `value` for use in a URI query string. - - Considers &, = and characters not in `query` set from RFC 3986 §3.4 to be unsafe. - https://datatracker.ietf.org/doc/html/rfc3986#section-3.4 - """ - return quote(value, safe="!$'()*+,;:@/?") - - -def _quote_fragment_part(value: str) -> str: - """Percent-encode `value` for use in a URI fragment. - - Considers characters not in `fragment` set from RFC 3986 §3.5 to be unsafe. - https://datatracker.ietf.org/doc/html/rfc3986#section-3.5 - """ - return quote(value, safe="!$&'()*+,;=:@/?") - - -def _interpolate( - template: str, - values: Mapping[str, Any], - quoter: Callable[[str], str], -) -> str: - """Replace {name} placeholders in `template`, quoting each value with `quoter`. - - Placeholder names are looked up in `values`. - - Raises: - KeyError: If a placeholder is not found in `values`. - """ - # re.split with a capturing group returns alternating - # [text, name, text, name, ..., text] elements. - parts = _PLACEHOLDER_RE.split(template) - - for i in range(1, len(parts), 2): - name = parts[i] - if name not in values: - raise KeyError(f"a value for placeholder {{{name}}} was not provided") - val = values[name] - if val is None: - parts[i] = "null" - elif isinstance(val, bool): - parts[i] = "true" if val else "false" - else: - parts[i] = quoter(str(values[name])) - - return "".join(parts) - - -def path_template(template: str, /, **kwargs: Any) -> str: - """Interpolate {name} placeholders in `template` from keyword arguments. - - Args: - template: The template string containing {name} placeholders. - **kwargs: Keyword arguments to interpolate into the template. - - Returns: - The template with placeholders interpolated and percent-encoded. - - Safe characters for percent-encoding are dependent on the URI component. - Placeholders in path and fragment portions are percent-encoded where the `segment` - and `fragment` sets from RFC 3986 respectively are considered safe. - Placeholders in the query portion are percent-encoded where the `query` set from - RFC 3986 §3.3 is considered safe except for = and & characters. - - Raises: - KeyError: If a placeholder is not found in `kwargs`. - ValueError: If resulting path contains /./ or /../ segments (including percent-encoded dot-segments). - """ - # Split the template into path, query, and fragment portions. - fragment_template: str | None = None - query_template: str | None = None - - rest = template - if "#" in rest: - rest, fragment_template = rest.split("#", 1) - if "?" in rest: - rest, query_template = rest.split("?", 1) - path_template = rest - - # Interpolate each portion with the appropriate quoting rules. - path_result = _interpolate(path_template, kwargs, _quote_path_segment_part) - - # Reject dot-segments (. and ..) in the final assembled path. The check - # runs after interpolation so that adjacent placeholders or a mix of static - # text and placeholders that together form a dot-segment are caught. - # Also reject percent-encoded dot-segments to protect against incorrectly - # implemented normalization in servers/proxies. - for segment in path_result.split("/"): - if _DOT_SEGMENT_RE.match(segment): - raise ValueError(f"Constructed path {path_result!r} contains dot-segment {segment!r} which is not allowed") - - result = path_result - if query_template is not None: - result += "?" + _interpolate(query_template, kwargs, _quote_query_part) - if fragment_template is not None: - result += "#" + _interpolate(fragment_template, kwargs, _quote_fragment_part) - - return result diff --git a/src/supermemory/_utils/_proxy.py b/src/supermemory/_utils/_proxy.py deleted file mode 100644 index 0f239a33..00000000 --- a/src/supermemory/_utils/_proxy.py +++ /dev/null @@ -1,65 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from typing import Generic, TypeVar, Iterable, cast -from typing_extensions import override - -T = TypeVar("T") - - -class LazyProxy(Generic[T], ABC): - """Implements data methods to pretend that an instance is another instance. - - This includes forwarding attribute access and other methods. - """ - - # Note: we have to special case proxies that themselves return proxies - # to support using a proxy as a catch-all for any random access, e.g. `proxy.foo.bar.baz` - - def __getattr__(self, attr: str) -> object: - proxied = self.__get_proxied__() - if isinstance(proxied, LazyProxy): - return proxied # pyright: ignore - return getattr(proxied, attr) - - @override - def __repr__(self) -> str: - proxied = self.__get_proxied__() - if isinstance(proxied, LazyProxy): - return proxied.__class__.__name__ - return repr(self.__get_proxied__()) - - @override - def __str__(self) -> str: - proxied = self.__get_proxied__() - if isinstance(proxied, LazyProxy): - return proxied.__class__.__name__ - return str(proxied) - - @override - def __dir__(self) -> Iterable[str]: - proxied = self.__get_proxied__() - if isinstance(proxied, LazyProxy): - return [] - return proxied.__dir__() - - @property # type: ignore - @override - def __class__(self) -> type: # pyright: ignore - try: - proxied = self.__get_proxied__() - except Exception: - return type(self) - if issubclass(type(proxied), LazyProxy): - return type(proxied) - return proxied.__class__ - - def __get_proxied__(self) -> T: - return self.__load__() - - def __as_proxied__(self) -> T: - """Helper method that returns the current proxy, typed as the loaded object""" - return cast(T, self) - - @abstractmethod - def __load__(self) -> T: ... diff --git a/src/supermemory/_utils/_reflection.py b/src/supermemory/_utils/_reflection.py deleted file mode 100644 index 89aa712a..00000000 --- a/src/supermemory/_utils/_reflection.py +++ /dev/null @@ -1,42 +0,0 @@ -from __future__ import annotations - -import inspect -from typing import Any, Callable - - -def function_has_argument(func: Callable[..., Any], arg_name: str) -> bool: - """Returns whether or not the given function has a specific parameter""" - sig = inspect.signature(func) - return arg_name in sig.parameters - - -def assert_signatures_in_sync( - source_func: Callable[..., Any], - check_func: Callable[..., Any], - *, - exclude_params: set[str] = set(), -) -> None: - """Ensure that the signature of the second function matches the first.""" - - check_sig = inspect.signature(check_func) - source_sig = inspect.signature(source_func) - - errors: list[str] = [] - - for name, source_param in source_sig.parameters.items(): - if name in exclude_params: - continue - - custom_param = check_sig.parameters.get(name) - if not custom_param: - errors.append(f"the `{name}` param is missing") - continue - - if custom_param.annotation != source_param.annotation: - errors.append( - f"types for the `{name}` param are do not match; source={repr(source_param.annotation)} checking={repr(custom_param.annotation)}" - ) - continue - - if errors: - raise AssertionError(f"{len(errors)} errors encountered when comparing signatures:\n\n" + "\n\n".join(errors)) diff --git a/src/supermemory/_utils/_resources_proxy.py b/src/supermemory/_utils/_resources_proxy.py deleted file mode 100644 index 53b457d1..00000000 --- a/src/supermemory/_utils/_resources_proxy.py +++ /dev/null @@ -1,24 +0,0 @@ -from __future__ import annotations - -from typing import Any -from typing_extensions import override - -from ._proxy import LazyProxy - - -class ResourcesProxy(LazyProxy[Any]): - """A proxy for the `supermemory.resources` module. - - This is used so that we can lazily import `supermemory.resources` only when - needed *and* so that users can just import `supermemory` and reference `supermemory.resources` - """ - - @override - def __load__(self) -> Any: - import importlib - - mod = importlib.import_module("supermemory.resources") - return mod - - -resources = ResourcesProxy().__as_proxied__() diff --git a/src/supermemory/_utils/_streams.py b/src/supermemory/_utils/_streams.py deleted file mode 100644 index f4a0208f..00000000 --- a/src/supermemory/_utils/_streams.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import Any -from typing_extensions import Iterator, AsyncIterator - - -def consume_sync_iterator(iterator: Iterator[Any]) -> None: - for _ in iterator: - ... - - -async def consume_async_iterator(iterator: AsyncIterator[Any]) -> None: - async for _ in iterator: - ... diff --git a/src/supermemory/_utils/_sync.py b/src/supermemory/_utils/_sync.py deleted file mode 100644 index f6027c18..00000000 --- a/src/supermemory/_utils/_sync.py +++ /dev/null @@ -1,58 +0,0 @@ -from __future__ import annotations - -import asyncio -import functools -from typing import TypeVar, Callable, Awaitable -from typing_extensions import ParamSpec - -import anyio -import sniffio -import anyio.to_thread - -T_Retval = TypeVar("T_Retval") -T_ParamSpec = ParamSpec("T_ParamSpec") - - -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 anyio.to_thread.run_sync( - functools.partial(func, *args, **kwargs), - ) - - -# inspired by `asyncer`, https://github.com/tiangolo/asyncer -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. - - Usage: - - ```python - def blocking_func(arg1, arg2, kwarg1=None): - # blocking code - return result - - - result = asyncify(blocking_function)(arg1, arg2, kwarg1=value1) - ``` - - ## Arguments - - `function`: a blocking regular callable (e.g. a function) - - ## Return - - An async function that takes the same positional and keyword arguments as the - original one, that when called runs the same original function in a thread worker - and returns the result. - """ - - async def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval: - return await to_thread(function, *args, **kwargs) - - return wrapper diff --git a/src/supermemory/_utils/_transform.py b/src/supermemory/_utils/_transform.py deleted file mode 100644 index 52075492..00000000 --- a/src/supermemory/_utils/_transform.py +++ /dev/null @@ -1,457 +0,0 @@ -from __future__ import annotations - -import io -import base64 -import pathlib -from typing import Any, Mapping, TypeVar, cast -from datetime import date, datetime -from typing_extensions import Literal, get_args, override, get_type_hints as _get_type_hints - -import anyio -import pydantic - -from ._utils import ( - is_list, - is_given, - lru_cache, - is_mapping, - is_iterable, - is_sequence, -) -from .._files import is_base64_file_input -from ._compat import get_origin, is_typeddict -from ._typing import ( - is_list_type, - is_union_type, - extract_type_arg, - is_iterable_type, - is_required_type, - is_sequence_type, - is_annotated_type, - strip_annotated_type, -) - -_T = TypeVar("_T") - - -# TODO: support for drilling globals() and locals() -# TODO: ensure works correctly with forward references in all cases - - -PropertyFormat = Literal["iso8601", "base64", "custom"] - - -class PropertyInfo: - """Metadata class to be used in Annotated types to provide information about a given type. - - For example: - - class MyParams(TypedDict): - account_holder_name: Annotated[str, PropertyInfo(alias='accountHolderName')] - - This means that {'account_holder_name': 'Robert'} will be transformed to {'accountHolderName': 'Robert'} before being sent to the API. - """ - - alias: str | None - format: PropertyFormat | None - format_template: str | None - discriminator: str | None - - def __init__( - self, - *, - alias: str | None = None, - format: PropertyFormat | None = None, - format_template: str | None = None, - discriminator: str | None = None, - ) -> None: - self.alias = alias - self.format = format - self.format_template = format_template - self.discriminator = discriminator - - @override - def __repr__(self) -> str: - return f"{self.__class__.__name__}(alias='{self.alias}', format={self.format}, format_template='{self.format_template}', discriminator='{self.discriminator}')" - - -def maybe_transform( - data: object, - expected_type: object, -) -> Any | None: - """Wrapper over `transform()` that allows `None` to be passed. - - See `transform()` for more details. - """ - if data is None: - return None - return transform(data, expected_type) - - -# Wrapper over _transform_recursive providing fake types -def transform( - data: _T, - expected_type: object, -) -> _T: - """Transform dictionaries based off of type information from the given type, for example: - - ```py - class Params(TypedDict, total=False): - card_id: Required[Annotated[str, PropertyInfo(alias="cardID")]] - - - transformed = transform({"card_id": ""}, Params) - # {'cardID': ''} - ``` - - Any keys / data that does not have type information given will be included as is. - - It should be noted that the transformations that this function does are not represented in the type system. - """ - transformed = _transform_recursive(data, annotation=cast(type, expected_type)) - return cast(_T, transformed) - - -@lru_cache(maxsize=8096) -def _get_annotated_type(type_: type) -> type | None: - """If the given type is an `Annotated` type then it is returned, if not `None` is returned. - - This also unwraps the type when applicable, e.g. `Required[Annotated[T, ...]]` - """ - if is_required_type(type_): - # Unwrap `Required[Annotated[T, ...]]` to `Annotated[T, ...]` - type_ = get_args(type_)[0] - - if is_annotated_type(type_): - return type_ - - return None - - -def _maybe_transform_key(key: str, type_: type) -> str: - """Transform the given `data` based on the annotations provided in `type_`. - - Note: this function only looks at `Annotated` types that contain `PropertyInfo` metadata. - """ - annotated_type = _get_annotated_type(type_) - if annotated_type is None: - # no `Annotated` definition for this type, no transformation needed - return key - - # ignore the first argument as it is the actual type - annotations = get_args(annotated_type)[1:] - for annotation in annotations: - if isinstance(annotation, PropertyInfo) and annotation.alias is not None: - return annotation.alias - - return key - - -def _no_transform_needed(annotation: type) -> bool: - return annotation == float or annotation == int - - -def _transform_recursive( - data: object, - *, - annotation: type, - inner_type: type | None = None, -) -> object: - """Transform the given data against the expected type. - - Args: - annotation: The direct type annotation given to the particular piece of data. - This may or may not be wrapped in metadata types, e.g. `Required[T]`, `Annotated[T, ...]` etc - - inner_type: If applicable, this is the "inside" type. This is useful in certain cases where the outside type - is a container type such as `List[T]`. In that case `inner_type` should be set to `T` so that each entry in - the list can be transformed using the metadata from the container type. - - Defaults to the same value as the `annotation` argument. - """ - from .._compat import model_dump - - if inner_type is None: - inner_type = annotation - - stripped_type = strip_annotated_type(inner_type) - origin = get_origin(stripped_type) or stripped_type - if is_typeddict(stripped_type) and is_mapping(data): - return _transform_typeddict(data, stripped_type) - - if origin == dict and is_mapping(data): - items_type = get_args(stripped_type)[1] - return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()} - - if ( - # List[T] - (is_list_type(stripped_type) and is_list(data)) - # Iterable[T] - or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) - # Sequence[T] - or (is_sequence_type(stripped_type) and is_sequence(data) and not isinstance(data, str)) - ): - # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually - # intended as an iterable, so we don't transform it. - if isinstance(data, dict): - return cast(object, data) - - inner_type = extract_type_arg(stripped_type, 0) - if _no_transform_needed(inner_type): - # for some types there is no need to transform anything, so we can get a small - # perf boost from skipping that work. - # - # but we still need to convert to a list to ensure the data is json-serializable - if is_list(data): - return data - return list(data) - - return [_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] - - if is_union_type(stripped_type): - # For union types we run the transformation against all subtypes to ensure that everything is transformed. - # - # TODO: there may be edge cases where the same normalized field name will transform to two different names - # in different subtypes. - for subtype in get_args(stripped_type): - data = _transform_recursive(data, annotation=annotation, inner_type=subtype) - return data - - if isinstance(data, pydantic.BaseModel): - return model_dump(data, exclude_unset=True, mode="json") - - annotated_type = _get_annotated_type(annotation) - if annotated_type is None: - return data - - # ignore the first argument as it is the actual type - annotations = get_args(annotated_type)[1:] - for annotation in annotations: - if isinstance(annotation, PropertyInfo) and annotation.format is not None: - return _format_data(data, annotation.format, annotation.format_template) - - return data - - -def _format_data(data: object, format_: PropertyFormat, format_template: str | None) -> object: - if isinstance(data, (date, datetime)): - if format_ == "iso8601": - return data.isoformat() - - if format_ == "custom" and format_template is not None: - return data.strftime(format_template) - - if format_ == "base64" and is_base64_file_input(data): - binary: str | bytes | None = None - - if isinstance(data, pathlib.Path): - binary = data.read_bytes() - elif isinstance(data, io.IOBase): - binary = data.read() - - if isinstance(binary, str): # type: ignore[unreachable] - binary = binary.encode() - - if not isinstance(binary, bytes): - raise RuntimeError(f"Could not read bytes from {data}; Received {type(binary)}") - - return base64.b64encode(binary).decode("ascii") - - return data - - -def _transform_typeddict( - data: Mapping[str, object], - expected_type: type, -) -> Mapping[str, object]: - result: dict[str, object] = {} - annotations = get_type_hints(expected_type, include_extras=True) - for key, value in data.items(): - if not is_given(value): - # we don't need to include omitted values here as they'll - # be stripped out before the request is sent anyway - continue - - type_ = annotations.get(key) - if type_ is None: - # we do not have a type annotation for this field, leave it as is - result[key] = value - else: - result[_maybe_transform_key(key, type_)] = _transform_recursive(value, annotation=type_) - return result - - -async def async_maybe_transform( - data: object, - expected_type: object, -) -> Any | None: - """Wrapper over `async_transform()` that allows `None` to be passed. - - See `async_transform()` for more details. - """ - if data is None: - return None - return await async_transform(data, expected_type) - - -async def async_transform( - data: _T, - expected_type: object, -) -> _T: - """Transform dictionaries based off of type information from the given type, for example: - - ```py - class Params(TypedDict, total=False): - card_id: Required[Annotated[str, PropertyInfo(alias="cardID")]] - - - transformed = transform({"card_id": ""}, Params) - # {'cardID': ''} - ``` - - Any keys / data that does not have type information given will be included as is. - - It should be noted that the transformations that this function does are not represented in the type system. - """ - transformed = await _async_transform_recursive(data, annotation=cast(type, expected_type)) - return cast(_T, transformed) - - -async def _async_transform_recursive( - data: object, - *, - annotation: type, - inner_type: type | None = None, -) -> object: - """Transform the given data against the expected type. - - Args: - annotation: The direct type annotation given to the particular piece of data. - This may or may not be wrapped in metadata types, e.g. `Required[T]`, `Annotated[T, ...]` etc - - inner_type: If applicable, this is the "inside" type. This is useful in certain cases where the outside type - is a container type such as `List[T]`. In that case `inner_type` should be set to `T` so that each entry in - the list can be transformed using the metadata from the container type. - - Defaults to the same value as the `annotation` argument. - """ - from .._compat import model_dump - - if inner_type is None: - inner_type = annotation - - stripped_type = strip_annotated_type(inner_type) - origin = get_origin(stripped_type) or stripped_type - if is_typeddict(stripped_type) and is_mapping(data): - return await _async_transform_typeddict(data, stripped_type) - - if origin == dict and is_mapping(data): - items_type = get_args(stripped_type)[1] - return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()} - - if ( - # List[T] - (is_list_type(stripped_type) and is_list(data)) - # Iterable[T] - or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) - # Sequence[T] - or (is_sequence_type(stripped_type) and is_sequence(data) and not isinstance(data, str)) - ): - # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually - # intended as an iterable, so we don't transform it. - if isinstance(data, dict): - return cast(object, data) - - inner_type = extract_type_arg(stripped_type, 0) - if _no_transform_needed(inner_type): - # for some types there is no need to transform anything, so we can get a small - # perf boost from skipping that work. - # - # but we still need to convert to a list to ensure the data is json-serializable - if is_list(data): - return data - return list(data) - - return [await _async_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] - - if is_union_type(stripped_type): - # For union types we run the transformation against all subtypes to ensure that everything is transformed. - # - # TODO: there may be edge cases where the same normalized field name will transform to two different names - # in different subtypes. - for subtype in get_args(stripped_type): - data = await _async_transform_recursive(data, annotation=annotation, inner_type=subtype) - return data - - if isinstance(data, pydantic.BaseModel): - return model_dump(data, exclude_unset=True, mode="json") - - annotated_type = _get_annotated_type(annotation) - if annotated_type is None: - return data - - # ignore the first argument as it is the actual type - annotations = get_args(annotated_type)[1:] - for annotation in annotations: - if isinstance(annotation, PropertyInfo) and annotation.format is not None: - return await _async_format_data(data, annotation.format, annotation.format_template) - - return data - - -async def _async_format_data(data: object, format_: PropertyFormat, format_template: str | None) -> object: - if isinstance(data, (date, datetime)): - if format_ == "iso8601": - return data.isoformat() - - if format_ == "custom" and format_template is not None: - return data.strftime(format_template) - - if format_ == "base64" and is_base64_file_input(data): - binary: str | bytes | None = None - - if isinstance(data, pathlib.Path): - binary = await anyio.Path(data).read_bytes() - elif isinstance(data, io.IOBase): - binary = data.read() - - if isinstance(binary, str): # type: ignore[unreachable] - binary = binary.encode() - - if not isinstance(binary, bytes): - raise RuntimeError(f"Could not read bytes from {data}; Received {type(binary)}") - - return base64.b64encode(binary).decode("ascii") - - return data - - -async def _async_transform_typeddict( - data: Mapping[str, object], - expected_type: type, -) -> Mapping[str, object]: - result: dict[str, object] = {} - annotations = get_type_hints(expected_type, include_extras=True) - for key, value in data.items(): - if not is_given(value): - # we don't need to include omitted values here as they'll - # be stripped out before the request is sent anyway - continue - - type_ = annotations.get(key) - if type_ is None: - # we do not have a type annotation for this field, leave it as is - result[key] = value - else: - result[_maybe_transform_key(key, type_)] = await _async_transform_recursive(value, annotation=type_) - return result - - -@lru_cache(maxsize=8096) -def get_type_hints( - obj: Any, - globalns: dict[str, Any] | None = None, - localns: Mapping[str, Any] | None = None, - include_extras: bool = False, -) -> dict[str, Any]: - return _get_type_hints(obj, globalns=globalns, localns=localns, include_extras=include_extras) diff --git a/src/supermemory/_utils/_typing.py b/src/supermemory/_utils/_typing.py deleted file mode 100644 index 193109f3..00000000 --- a/src/supermemory/_utils/_typing.py +++ /dev/null @@ -1,156 +0,0 @@ -from __future__ import annotations - -import sys -import typing -import typing_extensions -from typing import Any, TypeVar, Iterable, cast -from collections import abc as _c_abc -from typing_extensions import ( - TypeIs, - Required, - Annotated, - get_args, - get_origin, -) - -from ._utils import lru_cache -from .._types import InheritsGeneric -from ._compat import is_union as _is_union - - -def is_annotated_type(typ: type) -> bool: - return get_origin(typ) == Annotated - - -def is_list_type(typ: type) -> bool: - return (get_origin(typ) or typ) == list - - -def is_sequence_type(typ: type) -> bool: - origin = get_origin(typ) or typ - return origin == typing_extensions.Sequence or origin == typing.Sequence or origin == _c_abc.Sequence - - -def is_iterable_type(typ: type) -> bool: - """If the given type is `typing.Iterable[T]`""" - origin = get_origin(typ) or typ - return origin == Iterable or origin == _c_abc.Iterable - - -def is_union_type(typ: type) -> bool: - return _is_union(get_origin(typ)) - - -def is_required_type(typ: type) -> bool: - return get_origin(typ) == Required - - -def is_typevar(typ: type) -> bool: - # type ignore is required because type checkers - # think this expression will always return False - return type(typ) == TypeVar # type: ignore - - -_TYPE_ALIAS_TYPES: tuple[type[typing_extensions.TypeAliasType], ...] = (typing_extensions.TypeAliasType,) -if sys.version_info >= (3, 12): - _TYPE_ALIAS_TYPES = (*_TYPE_ALIAS_TYPES, typing.TypeAliasType) - - -def is_type_alias_type(tp: Any, /) -> TypeIs[typing_extensions.TypeAliasType]: - """Return whether the provided argument is an instance of `TypeAliasType`. - - ```python - type Int = int - is_type_alias_type(Int) - # > True - Str = TypeAliasType("Str", str) - is_type_alias_type(Str) - # > True - ``` - """ - return isinstance(tp, _TYPE_ALIAS_TYPES) - - -# Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]] -@lru_cache(maxsize=8096) -def strip_annotated_type(typ: type) -> type: - if is_required_type(typ) or is_annotated_type(typ): - return strip_annotated_type(cast(type, get_args(typ)[0])) - - return typ - - -def extract_type_arg(typ: type, index: int) -> type: - args = get_args(typ) - try: - return cast(type, args[index]) - except IndexError as err: - raise RuntimeError(f"Expected type {typ} to have a type argument at index {index} but it did not") from err - - -def extract_type_var_from_base( - typ: type, - *, - generic_bases: tuple[type, ...], - index: int, - failure_message: str | None = None, -) -> type: - """Given a type like `Foo[T]`, returns the generic type variable `T`. - - This also handles the case where a concrete subclass is given, e.g. - ```py - class MyResponse(Foo[bytes]): - ... - - extract_type_var(MyResponse, bases=(Foo,), index=0) -> bytes - ``` - - And where a generic subclass is given: - ```py - _T = TypeVar('_T') - class MyResponse(Foo[_T]): - ... - - extract_type_var(MyResponse[bytes], bases=(Foo,), index=0) -> bytes - ``` - """ - cls = cast(object, get_origin(typ) or typ) - if cls in generic_bases: # pyright: ignore[reportUnnecessaryContains] - # we're given the class directly - return extract_type_arg(typ, index) - - # if a subclass is given - # --- - # this is needed as __orig_bases__ is not present in the typeshed stubs - # because it is intended to be for internal use only, however there does - # not seem to be a way to resolve generic TypeVars for inherited subclasses - # without using it. - if isinstance(cls, InheritsGeneric): - target_base_class: Any | None = None - for base in cls.__orig_bases__: - if base.__origin__ in generic_bases: - target_base_class = base - break - - if target_base_class is None: - raise RuntimeError( - "Could not find the generic base class;\n" - "This should never happen;\n" - f"Does {cls} inherit from one of {generic_bases} ?" - ) - - extracted = extract_type_arg(target_base_class, index) - if is_typevar(extracted): - # If the extracted type argument is itself a type variable - # then that means the subclass itself is generic, so we have - # to resolve the type argument from the class itself, not - # the base class. - # - # Note: if there is more than 1 type argument, the subclass could - # change the ordering of the type arguments, this is not currently - # supported. - return extract_type_arg(typ, index) - - return extracted - - raise RuntimeError(failure_message or f"Could not resolve inner type variable at index {index} for {typ}") diff --git a/src/supermemory/_utils/_utils.py b/src/supermemory/_utils/_utils.py deleted file mode 100644 index 199cd231..00000000 --- a/src/supermemory/_utils/_utils.py +++ /dev/null @@ -1,433 +0,0 @@ -from __future__ import annotations - -import os -import re -import inspect -import functools -from typing import ( - Any, - Tuple, - Mapping, - TypeVar, - Callable, - Iterable, - Sequence, - cast, - overload, -) -from pathlib import Path -from datetime import date, datetime -from typing_extensions import TypeGuard, get_args - -import sniffio - -from .._types import Omit, NotGiven, FileTypes, ArrayFormat, HeadersLike - -_T = TypeVar("_T") -_TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) -_MappingT = TypeVar("_MappingT", bound=Mapping[str, object]) -_SequenceT = TypeVar("_SequenceT", bound=Sequence[object]) -CallableT = TypeVar("CallableT", bound=Callable[..., Any]) - - -def flatten(t: Iterable[Iterable[_T]]) -> list[_T]: - return [item for sublist in t for item in sublist] - - -def extract_files( - # TODO: this needs to take Dict but variance issues..... - # create protocol type ? - query: Mapping[str, object], - *, - paths: Sequence[Sequence[str]], - array_format: ArrayFormat = "brackets", -) -> list[tuple[str, FileTypes]]: - """Recursively extract files from the given dictionary based on specified paths. - - A path may look like this ['foo', 'files', '', 'data']. - - ``array_format`` controls how ```` segments contribute to the emitted - field name. Supported values: ``"brackets"`` (``foo[]``), ``"repeat"`` and - ``"comma"`` (``foo``), ``"indices"`` (``foo[0]``, ``foo[1]``). - - Note: this mutates the given dictionary. - """ - files: list[tuple[str, FileTypes]] = [] - for path in paths: - files.extend(_extract_items(query, path, index=0, flattened_key=None, array_format=array_format)) - return files - - -def _array_suffix(array_format: ArrayFormat, array_index: int) -> str: - if array_format == "brackets": - return "[]" - if array_format == "indices": - return f"[{array_index}]" - if array_format == "repeat" or array_format == "comma": - # Both repeat the bare field name for each file part; there is no - # meaningful way to comma-join binary parts. - return "" - raise NotImplementedError( - f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}" - ) - - -def _extract_items( - obj: object, - path: Sequence[str], - *, - index: int, - flattened_key: str | None, - array_format: ArrayFormat, -) -> list[tuple[str, FileTypes]]: - try: - key = path[index] - except IndexError: - if not is_given(obj): - # no value was provided - we can safely ignore - return [] - - # cyclical import - from .._files import assert_is_file_content - - # We have exhausted the path, return the entry we found. - assert flattened_key is not None - - if is_list(obj): - files: list[tuple[str, FileTypes]] = [] - for array_index, entry in enumerate(obj): - suffix = _array_suffix(array_format, array_index) - emitted_key = (flattened_key + suffix) if flattened_key else suffix - assert_is_file_content(entry, key=emitted_key) - files.append((emitted_key, cast(FileTypes, entry))) - return files - - assert_is_file_content(obj, key=flattened_key) - return [(flattened_key, cast(FileTypes, obj))] - - index += 1 - if is_dict(obj): - try: - # Remove the field if there are no more dict keys in the path, - # only "" traversal markers or end. - if all(p == "" for p in path[index:]): - item = obj.pop(key) - else: - item = obj[key] - except KeyError: - # Key was not present in the dictionary, this is not indicative of an error - # as the given path may not point to a required field. We also do not want - # to enforce required fields as the API may differ from the spec in some cases. - return [] - if flattened_key is None: - flattened_key = key - else: - flattened_key += f"[{key}]" - return _extract_items( - item, - path, - index=index, - flattened_key=flattened_key, - array_format=array_format, - ) - elif is_list(obj): - if key != "": - return [] - - return flatten( - [ - _extract_items( - item, - path, - index=index, - flattened_key=( - (flattened_key if flattened_key is not None else "") + _array_suffix(array_format, array_index) - ), - array_format=array_format, - ) - for array_index, item in enumerate(obj) - ] - ) - - # Something unexpected was passed, just ignore it. - return [] - - -def is_given(obj: _T | NotGiven | Omit) -> TypeGuard[_T]: - return not isinstance(obj, NotGiven) and not isinstance(obj, Omit) - - -# 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 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 -# `is_*_t` is for when you're narrowing a known union type to a specific subset - - -def is_tuple(obj: object) -> TypeGuard[tuple[object, ...]]: - return isinstance(obj, tuple) - - -def is_tuple_t(obj: _TupleT | object) -> TypeGuard[_TupleT]: - return isinstance(obj, tuple) - - -def is_sequence(obj: object) -> TypeGuard[Sequence[object]]: - return isinstance(obj, Sequence) - - -def is_sequence_t(obj: _SequenceT | object) -> TypeGuard[_SequenceT]: - return isinstance(obj, Sequence) - - -def is_mapping(obj: object) -> TypeGuard[Mapping[str, object]]: - return isinstance(obj, Mapping) - - -def is_mapping_t(obj: _MappingT | object) -> TypeGuard[_MappingT]: - return isinstance(obj, Mapping) - - -def is_dict(obj: object) -> TypeGuard[dict[object, object]]: - return isinstance(obj, dict) - - -def is_list(obj: object) -> TypeGuard[list[object]]: - return isinstance(obj, list) - - -def is_iterable(obj: object) -> TypeGuard[Iterable[object]]: - return isinstance(obj, Iterable) - - -# copied from https://github.com/Rapptz/RoboDanny -def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str: - size = len(seq) - if size == 0: - return "" - - if size == 1: - return seq[0] - - if size == 2: - return f"{seq[0]} {final} {seq[1]}" - - return delim.join(seq[:-1]) + f" {final} {seq[-1]}" - - -def quote(string: str) -> str: - """Add single quotation marks around the given string. Does *not* do any escaping.""" - return f"'{string}'" - - -def required_args(*variants: Sequence[str]) -> Callable[[CallableT], CallableT]: - """Decorator to enforce a given set of arguments or variants of arguments are passed to the decorated function. - - Useful for enforcing runtime validation of overloaded functions. - - Example usage: - ```py - @overload - def foo(*, a: str) -> str: ... - - - @overload - def foo(*, b: bool) -> str: ... - - - # This enforces the same constraints that a static type checker would - # i.e. that either a or b must be passed to the function - @required_args(["a"], ["b"]) - def foo(*, a: str | None = None, b: bool | None = None) -> str: ... - ``` - """ - - def inner(func: CallableT) -> CallableT: - params = inspect.signature(func).parameters - positional = [ - name - for name, param in params.items() - if param.kind - in { - param.POSITIONAL_ONLY, - param.POSITIONAL_OR_KEYWORD, - } - ] - - @functools.wraps(func) - def wrapper(*args: object, **kwargs: object) -> object: - given_params: set[str] = set() - for i, _ in enumerate(args): - try: - given_params.add(positional[i]) - except IndexError: - raise TypeError( - f"{func.__name__}() takes {len(positional)} argument(s) but {len(args)} were given" - ) from None - - for key in kwargs.keys(): - given_params.add(key) - - for variant in variants: - matches = all((param in given_params for param in variant)) - if matches: - break - else: # no break - if len(variants) > 1: - variations = human_join( - ["(" + human_join([quote(arg) for arg in variant], final="and") + ")" for variant in variants] - ) - msg = f"Missing required arguments; Expected either {variations} arguments to be given" - else: - assert len(variants) > 0 - - # TODO: this error message is not deterministic - missing = list(set(variants[0]) - given_params) - if len(missing) > 1: - msg = f"Missing required arguments: {human_join([quote(arg) for arg in missing])}" - else: - msg = f"Missing required argument: {quote(missing[0])}" - raise TypeError(msg) - return func(*args, **kwargs) - - return wrapper # type: ignore - - return inner - - -_K = TypeVar("_K") -_V = TypeVar("_V") - - -@overload -def strip_not_given(obj: None) -> None: ... - - -@overload -def strip_not_given(obj: Mapping[_K, _V | NotGiven]) -> dict[_K, _V]: ... - - -@overload -def strip_not_given(obj: object) -> object: ... - - -def strip_not_given(obj: object | None) -> object: - """Remove all top-level keys where their values are instances of `NotGiven`""" - if obj is None: - return None - - if not is_mapping(obj): - return obj - - return {key: value for key, value in obj.items() if not isinstance(value, NotGiven)} - - -def coerce_integer(val: str) -> int: - return int(val, base=10) - - -def coerce_float(val: str) -> float: - return float(val) - - -def coerce_boolean(val: str) -> bool: - return val == "true" or val == "1" or val == "on" - - -def maybe_coerce_integer(val: str | None) -> int | None: - if val is None: - return None - return coerce_integer(val) - - -def maybe_coerce_float(val: str | None) -> float | None: - if val is None: - return None - return coerce_float(val) - - -def maybe_coerce_boolean(val: str | None) -> bool | None: - if val is None: - return None - return coerce_boolean(val) - - -def removeprefix(string: str, prefix: str) -> str: - """Remove a prefix from a string. - - Backport of `str.removeprefix` for Python < 3.9 - """ - if string.startswith(prefix): - return string[len(prefix) :] - return string - - -def removesuffix(string: str, suffix: str) -> str: - """Remove a suffix from a string. - - Backport of `str.removesuffix` for Python < 3.9 - """ - if string.endswith(suffix): - return string[: -len(suffix)] - return string - - -def file_from_path(path: str) -> FileTypes: - contents = Path(path).read_bytes() - file_name = os.path.basename(path) - return (file_name, contents) - - -def get_required_header(headers: HeadersLike, header: str) -> str: - lower_header = header.lower() - if is_mapping_t(headers): - # mypy doesn't understand the type narrowing here - for k, v in headers.items(): # type: ignore - if k.lower() == lower_header and isinstance(v, str): - return v - - # to deal with the case where the header looks like Stainless-Event-Id - intercaps_header = re.sub(r"([^\w])(\w)", lambda pat: pat.group(1) + pat.group(2).upper(), header.capitalize()) - - for normalized_header in [header, lower_header, header.upper(), intercaps_header]: - value = headers.get(normalized_header) - if value: - return value - - raise ValueError(f"Could not find {header} header") - - -def get_async_library() -> str: - try: - return sniffio.current_async_library() - except Exception: - return "false" - - -def lru_cache(*, maxsize: int | None = 128) -> Callable[[CallableT], CallableT]: - """A version of functools.lru_cache that retains the type signature - for the wrapped function arguments. - """ - wrapper = functools.lru_cache( # noqa: TID251 - maxsize=maxsize, - ) - return cast(Any, wrapper) # type: ignore[no-any-return] - - -def json_safe(data: object) -> object: - """Translates a mapping / sequence recursively in the same fashion - as `pydantic` v2's `model_dump(mode="json")`. - """ - if is_mapping(data): - return {json_safe(key): json_safe(value) for key, value in data.items()} - - if is_iterable(data) and not isinstance(data, (str, bytes, bytearray)): - return [json_safe(item) for item in data] - - if isinstance(data, (datetime, date)): - return data.isoformat() - - return data diff --git a/src/supermemory/_version.py b/src/supermemory/_version.py deleted file mode 100644 index dfb16293..00000000 --- a/src/supermemory/_version.py +++ /dev/null @@ -1,4 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -__title__ = "supermemory" -__version__ = "3.62.0" # x-release-please-version diff --git a/src/supermemory/client.py b/src/supermemory/client.py new file mode 100644 index 00000000..875551dc --- /dev/null +++ b/src/supermemory/client.py @@ -0,0 +1,998 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import os +import typing + +import httpx +from .core.api_error import ApiError +from .core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from .core.logging import LogConfig, Logger +from .core.request_options import RequestOptions +from .environment import SupermemoryEnvironment +from .raw_client import AsyncRawSupermemory, RawSupermemory +from .requests.filter_expression import FilterExpressionParams +from .requests.list_request_limit import ListRequestLimitParams +from .requests.list_request_page import ListRequestPageParams +from .requests.metadata_value import MetadataValueParams +from .requests.search_request_attach import SearchRequestAttachParams +from .types.document_ref import DocumentRef +from .types.dreaming_mode import DreamingMode +from .types.list_response import ListResponse +from .types.list_sort import ListSort +from .types.list_type import ListType +from .types.profile_response import ProfileResponse +from .types.search_mode import SearchMode +from .types.search_request_rerank import SearchRequestRerank +from .types.search_response import SearchResponse +from .types.sort_order import SortOrder +from .types.task_type import TaskType + +if typing.TYPE_CHECKING: + from .connectors.client import AsyncConnectorsClient, ConnectorsClient + from .documents.client import AsyncDocumentsClient, DocumentsClient + from .memories.client import AsyncMemoriesClient, MemoriesClient + from .namespaces.client import AsyncNamespacesClient, NamespacesClient + from .organization.client import AsyncOrganizationClient, OrganizationClient + from .profiles.client import AsyncProfilesClient, ProfilesClient +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class Supermemory: + """ + Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. + + Parameters + ---------- + base_url : typing.Optional[str] + The base url to use for requests from the client. + + environment : SupermemoryEnvironment + The environment to use for requests from the client. from .environment import SupermemoryEnvironment + + + + Defaults to SupermemoryEnvironment.DEFAULT + + + + api_key : typing.Optional[typing.Union[str, typing.Callable[[], str]]] + headers : typing.Optional[typing.Dict[str, str]] + Additional headers to send with every request. + + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + + max_retries : typing.Optional[int] + The default maximum number of retries for failed requests. Defaults to 2. Per-request `max_retries` in `request_options` takes precedence over this value. + + stream_reconnection_enabled : typing.Optional[bool] + Whether to automatically reconnect on stream disconnection for resumable streaming endpoints. Defaults to True. Per-request `stream_reconnection_enabled` in `request_options` takes precedence over this value. + + max_stream_reconnection_attempts : typing.Optional[int] + The maximum number of reconnection attempts for resumable streaming endpoints. Defaults to no limit. Per-request `max_stream_reconnection_attempts` in `request_options` takes precedence over this value. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. + + httpx_client : typing.Optional[httpx.Client] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + + logging : typing.Optional[typing.Union[LogConfig, Logger]] + Configure logging for the SDK. Accepts a LogConfig dict with 'level' (debug/info/warn/error), 'logger' (custom logger implementation), and 'silent' (boolean, defaults to True) fields. You can also pass a pre-configured Logger instance. + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + """ + + def __init__( + self, + *, + base_url: typing.Optional[str] = None, + environment: SupermemoryEnvironment = SupermemoryEnvironment.DEFAULT, + api_key: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = os.getenv("SUPERMEMORY_API_KEY"), + headers: typing.Optional[typing.Dict[str, str]] = None, + timeout: typing.Optional[float] = None, + max_retries: typing.Optional[int] = None, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.Client] = None, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, + ): + _defaulted_timeout = timeout if timeout is not None else 60 if httpx_client is None else None + _defaulted_max_retries = max_retries if max_retries is not None else 2 + if api_key is None: + raise ApiError( + body="The client must be instantiated be either passing in api_key or setting SUPERMEMORY_API_KEY" + ) + self._client_wrapper = SyncClientWrapper( + base_url=_get_base_url(base_url=base_url, environment=environment), + api_key=api_key, + headers=headers, + httpx_client=httpx_client + if httpx_client is not None + else httpx.Client(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.Client(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + max_retries=_defaulted_max_retries, + stream_reconnection_enabled=stream_reconnection_enabled, + max_stream_reconnection_attempts=max_stream_reconnection_attempts, + logging=logging, + ) + self._raw_client = RawSupermemory(client_wrapper=self._client_wrapper) + self._documents: typing.Optional[DocumentsClient] = None + self._profiles: typing.Optional[ProfilesClient] = None + self._memories: typing.Optional[MemoriesClient] = None + self._connectors: typing.Optional[ConnectorsClient] = None + self._namespaces: typing.Optional[NamespacesClient] = None + self._organization: typing.Optional[OrganizationClient] = None + + @property + def with_raw_response(self) -> RawSupermemory: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawSupermemory + """ + return self._raw_client + + def add( + self, + namespace: str, + *, + content: str, + task_type: typing.Optional[TaskType] = None, + dreaming: typing.Optional[DreamingMode] = None, + id: typing.Optional[str] = OMIT, + supporting_context: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, MetadataValueParams]] = OMIT, + group: typing.Optional[typing.Dict[str, MetadataValueParams]] = OMIT, + date: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> DocumentRef: + """ + Turn text or a supported URL into searchable, evolving memory. Supply a new ID to create a document, or reuse an existing ID to append new information while preserving its history. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + content : str + The content to process. This may be plaintext or a URL to supported rich content. + + task_type : typing.Optional[TaskType] + Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + + dreaming : typing.Optional[DreamingMode] + Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + + id : typing.Optional[str] + An optional caller-defined document ID + + supporting_context : typing.Optional[str] + Context used to guide memory extraction within this namespace. Max 1500 characters. + + metadata : typing.Optional[typing.Dict[str, MetadataValueParams]] + Arbitrary metadata attached to the document and derived data + + group : typing.Optional[typing.Dict[str, MetadataValueParams]] + Metadata values that isolate related context within the namespace + + date : typing.Optional[str] + When the source content is from, in ISO 8601 format + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DocumentRef + Document accepted for processing + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.add( + namespace="user_alex", + content="Supermemory turns unstructured content into evolving memory.", + ) + """ + _response = self._raw_client.add( + namespace, + content=content, + task_type=task_type, + dreaming=dreaming, + id=id, + supporting_context=supporting_context, + metadata=metadata, + group=group, + date=date, + request_options=request_options, + ) + return _response.data + + def search( + self, + namespace: str, + *, + query: str, + limit: typing.Optional[int] = None, + search_mode: typing.Optional[SearchMode] = None, + filter: typing.Optional[FilterExpressionParams] = OMIT, + attach: typing.Optional[SearchRequestAttachParams] = OMIT, + threshold: typing.Optional[float] = OMIT, + rerank: typing.Optional[SearchRequestRerank] = OMIT, + rewrite_query: typing.Optional[bool] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> SearchResponse: + """ + Recall the most relevant learned context and source passages from a namespace. Hybrid search combines memories with document chunks by default, with optional query rewriting, reranking, and supporting context attachments. + + Parameters + ---------- + namespace : str + The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + query : str + Natural-language question, topic, or phrase to retrieve relevant context for. Replaces the v4 `q` field. + + limit : typing.Optional[int] + Maximum number of results to return + + search_mode : typing.Optional[SearchMode] + Search surface. "hybrid" combines learned memories with source chunks, "memories" returns learned context, and "chunks" returns source passages. + + filter : typing.Optional[FilterExpressionParams] + Type-safe metadata conditions applied before ranking results. Replaces the v4 `filters` field and unifies both legacy filter formats into one expression type. + + attach : typing.Optional[SearchRequestAttachParams] + Optional context to include alongside each matching result + + threshold : typing.Optional[float] + Minimum relevance score from 0 to 1. Raise it for precision (fewer, accurate results) or lower it for broader recall (more results). + + rerank : typing.Optional[SearchRequestRerank] + Post-retrieval ranking. "order" improves result ordering; "aggregate" also combines overlapping context into cleaner answers. This is helpful if you want to ensure the most relevant results are returned. + + rewrite_query : typing.Optional[bool] + Expand and clarify the query before retrieval to improve recall for conversational or underspecified prompts. This increases the latency by about 400ms. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SearchResponse + Search results + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.search( + namespace="user_alex", + limit=10, + query="what are the API rate limits", + ) + """ + _response = self._raw_client.search( + namespace, + query=query, + limit=limit, + search_mode=search_mode, + filter=filter, + attach=attach, + threshold=threshold, + rerank=rerank, + rewrite_query=rewrite_query, + request_options=request_options, + ) + return _response.data + + def profile( + self, + namespace: str, + *, + filter: typing.Optional[FilterExpressionParams] = OMIT, + buckets: typing.Optional[typing.Sequence[str]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> ProfileResponse: + """ + Read a continuously maintained understanding of the subject represented by this namespace. Stable facts, evolving context, and selected custom buckets are returned together without requiring a search query. + + Parameters + ---------- + namespace : str + The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + filter : typing.Optional[FilterExpressionParams] + Type-safe metadata conditions that limit which memories contribute to the profile + + buckets : typing.Optional[typing.Sequence[str]] + Custom buckets to return. Omit to return every effective bucket. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ProfileResponse + Namespace profile. Send `Accept: text/markdown` to receive it as a markdown document instead of JSON. + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.profile( + namespace="user_alex", + ) + """ + _response = self._raw_client.profile(namespace, filter=filter, buckets=buckets, request_options=request_options) + return _response.data + + def list( + self, + namespace: str, + type: ListType, + *, + page: typing.Optional[ListRequestPageParams] = None, + limit: typing.Optional[ListRequestLimitParams] = None, + sort: typing.Optional[ListSort] = None, + order: typing.Optional[SortOrder] = None, + filter: typing.Optional[FilterExpressionParams] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> ListResponse: + """ + Browse documents, source chunks, or learned memories through one predictable paginated contract. Choose the collection in the path; the other collection arrays remain empty for a stable response shape. + + Parameters + ---------- + namespace : str + Namespace containing the resources to list. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope resources. + + type : ListType + Resource collection to list: source documents, extracted chunks, or learned memories + + page : typing.Optional[ListRequestPageParams] + One-based page number + + limit : typing.Optional[ListRequestLimitParams] + Maximum resources to return per page + + sort : typing.Optional[ListSort] + Field used to order results. Position is available only when listing chunks. + + order : typing.Optional[SortOrder] + Ascending or descending sort direction + + filter : typing.Optional[FilterExpressionParams] + Type-safe metadata conditions applied before pagination + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListResponse + Namespace resources retrieved successfully + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.list( + namespace="user_alex", + type="documents", + ) + """ + _response = self._raw_client.list( + namespace, + type, + page=page, + limit=limit, + sort=sort, + order=order, + filter=filter, + request_options=request_options, + ) + return _response.data + + @property + def documents(self): + if self._documents is None: + from .documents.client import DocumentsClient # noqa: E402 + + self._documents = DocumentsClient(client_wrapper=self._client_wrapper) + return self._documents + + @property + def profiles(self): + if self._profiles is None: + from .profiles.client import ProfilesClient # noqa: E402 + + self._profiles = ProfilesClient(client_wrapper=self._client_wrapper) + return self._profiles + + @property + def memories(self): + if self._memories is None: + from .memories.client import MemoriesClient # noqa: E402 + + self._memories = MemoriesClient(client_wrapper=self._client_wrapper) + return self._memories + + @property + def connectors(self): + if self._connectors is None: + from .connectors.client import ConnectorsClient # noqa: E402 + + self._connectors = ConnectorsClient(client_wrapper=self._client_wrapper) + return self._connectors + + @property + def namespaces(self): + if self._namespaces is None: + from .namespaces.client import NamespacesClient # noqa: E402 + + self._namespaces = NamespacesClient(client_wrapper=self._client_wrapper) + return self._namespaces + + @property + def organization(self): + if self._organization is None: + from .organization.client import OrganizationClient # noqa: E402 + + self._organization = OrganizationClient(client_wrapper=self._client_wrapper) + return self._organization + + +def _make_default_async_client( + timeout: typing.Optional[float], + follow_redirects: typing.Optional[bool], +) -> httpx.AsyncClient: + try: + import httpx_aiohttp # type: ignore[import-not-found] + except ImportError: + pass + else: + if follow_redirects is not None: + return httpx_aiohttp.HttpxAiohttpClient(timeout=timeout, follow_redirects=follow_redirects) + return httpx_aiohttp.HttpxAiohttpClient(timeout=timeout) + + if follow_redirects is not None: + return httpx.AsyncClient(timeout=timeout, follow_redirects=follow_redirects) + return httpx.AsyncClient(timeout=timeout) + + +class AsyncSupermemory: + """ + Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. + + Parameters + ---------- + base_url : typing.Optional[str] + The base url to use for requests from the client. + + environment : SupermemoryEnvironment + The environment to use for requests from the client. from .environment import SupermemoryEnvironment + + + + Defaults to SupermemoryEnvironment.DEFAULT + + + + api_key : typing.Optional[typing.Union[str, typing.Callable[[], str]]] + headers : typing.Optional[typing.Dict[str, str]] + Additional headers to send with every request. + + async_token : typing.Optional[typing.Callable[[], typing.Awaitable[str]]] + An async callable that returns a bearer token. Use this when token acquisition involves async I/O (e.g., refreshing tokens via an async HTTP client). When provided, this is used instead of the synchronous token for async requests. + + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + + max_retries : typing.Optional[int] + The default maximum number of retries for failed requests. Defaults to 2. Per-request `max_retries` in `request_options` takes precedence over this value. + + stream_reconnection_enabled : typing.Optional[bool] + Whether to automatically reconnect on stream disconnection for resumable streaming endpoints. Defaults to True. Per-request `stream_reconnection_enabled` in `request_options` takes precedence over this value. + + max_stream_reconnection_attempts : typing.Optional[int] + The maximum number of reconnection attempts for resumable streaming endpoints. Defaults to no limit. Per-request `max_stream_reconnection_attempts` in `request_options` takes precedence over this value. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. + + httpx_client : typing.Optional[httpx.AsyncClient] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + + logging : typing.Optional[typing.Union[LogConfig, Logger]] + Configure logging for the SDK. Accepts a LogConfig dict with 'level' (debug/info/warn/error), 'logger' (custom logger implementation), and 'silent' (boolean, defaults to True) fields. You can also pass a pre-configured Logger instance. + + Examples + -------- + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + """ + + def __init__( + self, + *, + base_url: typing.Optional[str] = None, + environment: SupermemoryEnvironment = SupermemoryEnvironment.DEFAULT, + api_key: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = os.getenv("SUPERMEMORY_API_KEY"), + headers: typing.Optional[typing.Dict[str, str]] = None, + async_token: typing.Optional[typing.Callable[[], typing.Awaitable[str]]] = None, + timeout: typing.Optional[float] = None, + max_retries: typing.Optional[int] = None, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.AsyncClient] = None, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, + ): + _defaulted_timeout = timeout if timeout is not None else 60 if httpx_client is None else None + _defaulted_max_retries = max_retries if max_retries is not None else 2 + if api_key is None: + raise ApiError( + body="The client must be instantiated be either passing in api_key or setting SUPERMEMORY_API_KEY" + ) + self._client_wrapper = AsyncClientWrapper( + base_url=_get_base_url(base_url=base_url, environment=environment), + api_key=api_key, + headers=headers, + async_token=async_token, + httpx_client=httpx_client + if httpx_client is not None + else _make_default_async_client(timeout=_defaulted_timeout, follow_redirects=follow_redirects), + timeout=_defaulted_timeout, + max_retries=_defaulted_max_retries, + stream_reconnection_enabled=stream_reconnection_enabled, + max_stream_reconnection_attempts=max_stream_reconnection_attempts, + logging=logging, + ) + self._raw_client = AsyncRawSupermemory(client_wrapper=self._client_wrapper) + self._documents: typing.Optional[AsyncDocumentsClient] = None + self._profiles: typing.Optional[AsyncProfilesClient] = None + self._memories: typing.Optional[AsyncMemoriesClient] = None + self._connectors: typing.Optional[AsyncConnectorsClient] = None + self._namespaces: typing.Optional[AsyncNamespacesClient] = None + self._organization: typing.Optional[AsyncOrganizationClient] = None + + @property + def with_raw_response(self) -> AsyncRawSupermemory: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawSupermemory + """ + return self._raw_client + + async def add( + self, + namespace: str, + *, + content: str, + task_type: typing.Optional[TaskType] = None, + dreaming: typing.Optional[DreamingMode] = None, + id: typing.Optional[str] = OMIT, + supporting_context: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, MetadataValueParams]] = OMIT, + group: typing.Optional[typing.Dict[str, MetadataValueParams]] = OMIT, + date: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> DocumentRef: + """ + Turn text or a supported URL into searchable, evolving memory. Supply a new ID to create a document, or reuse an existing ID to append new information while preserving its history. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + content : str + The content to process. This may be plaintext or a URL to supported rich content. + + task_type : typing.Optional[TaskType] + Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + + dreaming : typing.Optional[DreamingMode] + Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + + id : typing.Optional[str] + An optional caller-defined document ID + + supporting_context : typing.Optional[str] + Context used to guide memory extraction within this namespace. Max 1500 characters. + + metadata : typing.Optional[typing.Dict[str, MetadataValueParams]] + Arbitrary metadata attached to the document and derived data + + group : typing.Optional[typing.Dict[str, MetadataValueParams]] + Metadata values that isolate related context within the namespace + + date : typing.Optional[str] + When the source content is from, in ISO 8601 format + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DocumentRef + Document accepted for processing + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.add( + namespace="user_alex", + content="Supermemory turns unstructured content into evolving memory.", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.add( + namespace, + content=content, + task_type=task_type, + dreaming=dreaming, + id=id, + supporting_context=supporting_context, + metadata=metadata, + group=group, + date=date, + request_options=request_options, + ) + return _response.data + + async def search( + self, + namespace: str, + *, + query: str, + limit: typing.Optional[int] = None, + search_mode: typing.Optional[SearchMode] = None, + filter: typing.Optional[FilterExpressionParams] = OMIT, + attach: typing.Optional[SearchRequestAttachParams] = OMIT, + threshold: typing.Optional[float] = OMIT, + rerank: typing.Optional[SearchRequestRerank] = OMIT, + rewrite_query: typing.Optional[bool] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> SearchResponse: + """ + Recall the most relevant learned context and source passages from a namespace. Hybrid search combines memories with document chunks by default, with optional query rewriting, reranking, and supporting context attachments. + + Parameters + ---------- + namespace : str + The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + query : str + Natural-language question, topic, or phrase to retrieve relevant context for. Replaces the v4 `q` field. + + limit : typing.Optional[int] + Maximum number of results to return + + search_mode : typing.Optional[SearchMode] + Search surface. "hybrid" combines learned memories with source chunks, "memories" returns learned context, and "chunks" returns source passages. + + filter : typing.Optional[FilterExpressionParams] + Type-safe metadata conditions applied before ranking results. Replaces the v4 `filters` field and unifies both legacy filter formats into one expression type. + + attach : typing.Optional[SearchRequestAttachParams] + Optional context to include alongside each matching result + + threshold : typing.Optional[float] + Minimum relevance score from 0 to 1. Raise it for precision (fewer, accurate results) or lower it for broader recall (more results). + + rerank : typing.Optional[SearchRequestRerank] + Post-retrieval ranking. "order" improves result ordering; "aggregate" also combines overlapping context into cleaner answers. This is helpful if you want to ensure the most relevant results are returned. + + rewrite_query : typing.Optional[bool] + Expand and clarify the query before retrieval to improve recall for conversational or underspecified prompts. This increases the latency by about 400ms. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SearchResponse + Search results + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.search( + namespace="user_alex", + limit=10, + query="what are the API rate limits", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.search( + namespace, + query=query, + limit=limit, + search_mode=search_mode, + filter=filter, + attach=attach, + threshold=threshold, + rerank=rerank, + rewrite_query=rewrite_query, + request_options=request_options, + ) + return _response.data + + async def profile( + self, + namespace: str, + *, + filter: typing.Optional[FilterExpressionParams] = OMIT, + buckets: typing.Optional[typing.Sequence[str]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> ProfileResponse: + """ + Read a continuously maintained understanding of the subject represented by this namespace. Stable facts, evolving context, and selected custom buckets are returned together without requiring a search query. + + Parameters + ---------- + namespace : str + The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + filter : typing.Optional[FilterExpressionParams] + Type-safe metadata conditions that limit which memories contribute to the profile + + buckets : typing.Optional[typing.Sequence[str]] + Custom buckets to return. Omit to return every effective bucket. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ProfileResponse + Namespace profile. Send `Accept: text/markdown` to receive it as a markdown document instead of JSON. + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.profile( + namespace="user_alex", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.profile( + namespace, filter=filter, buckets=buckets, request_options=request_options + ) + return _response.data + + async def list( + self, + namespace: str, + type: ListType, + *, + page: typing.Optional[ListRequestPageParams] = None, + limit: typing.Optional[ListRequestLimitParams] = None, + sort: typing.Optional[ListSort] = None, + order: typing.Optional[SortOrder] = None, + filter: typing.Optional[FilterExpressionParams] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> ListResponse: + """ + Browse documents, source chunks, or learned memories through one predictable paginated contract. Choose the collection in the path; the other collection arrays remain empty for a stable response shape. + + Parameters + ---------- + namespace : str + Namespace containing the resources to list. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope resources. + + type : ListType + Resource collection to list: source documents, extracted chunks, or learned memories + + page : typing.Optional[ListRequestPageParams] + One-based page number + + limit : typing.Optional[ListRequestLimitParams] + Maximum resources to return per page + + sort : typing.Optional[ListSort] + Field used to order results. Position is available only when listing chunks. + + order : typing.Optional[SortOrder] + Ascending or descending sort direction + + filter : typing.Optional[FilterExpressionParams] + Type-safe metadata conditions applied before pagination + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ListResponse + Namespace resources retrieved successfully + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.list( + namespace="user_alex", + type="documents", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.list( + namespace, + type, + page=page, + limit=limit, + sort=sort, + order=order, + filter=filter, + request_options=request_options, + ) + return _response.data + + @property + def documents(self): + if self._documents is None: + from .documents.client import AsyncDocumentsClient # noqa: E402 + + self._documents = AsyncDocumentsClient(client_wrapper=self._client_wrapper) + return self._documents + + @property + def profiles(self): + if self._profiles is None: + from .profiles.client import AsyncProfilesClient # noqa: E402 + + self._profiles = AsyncProfilesClient(client_wrapper=self._client_wrapper) + return self._profiles + + @property + def memories(self): + if self._memories is None: + from .memories.client import AsyncMemoriesClient # noqa: E402 + + self._memories = AsyncMemoriesClient(client_wrapper=self._client_wrapper) + return self._memories + + @property + def connectors(self): + if self._connectors is None: + from .connectors.client import AsyncConnectorsClient # noqa: E402 + + self._connectors = AsyncConnectorsClient(client_wrapper=self._client_wrapper) + return self._connectors + + @property + def namespaces(self): + if self._namespaces is None: + from .namespaces.client import AsyncNamespacesClient # noqa: E402 + + self._namespaces = AsyncNamespacesClient(client_wrapper=self._client_wrapper) + return self._namespaces + + @property + def organization(self): + if self._organization is None: + from .organization.client import AsyncOrganizationClient # noqa: E402 + + self._organization = AsyncOrganizationClient(client_wrapper=self._client_wrapper) + return self._organization + + +def _get_base_url(*, base_url: typing.Optional[str] = None, environment: SupermemoryEnvironment) -> str: + if base_url is not None: + return base_url + elif environment is not None: + return environment.value + else: + raise Exception("Please pass in either base_url or environment to construct the client") diff --git a/src/supermemory/connectors/__init__.py b/src/supermemory/connectors/__init__.py new file mode 100644 index 00000000..14909025 --- /dev/null +++ b/src/supermemory/connectors/__init__.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import DeleteConnectorsRequestDeleteDocuments +_dynamic_imports: typing.Dict[str, str] = {"DeleteConnectorsRequestDeleteDocuments": ".types"} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["DeleteConnectorsRequestDeleteDocuments"] diff --git a/src/supermemory/connectors/client.py b/src/supermemory/connectors/client.py new file mode 100644 index 00000000..f1d8074c --- /dev/null +++ b/src/supermemory/connectors/client.py @@ -0,0 +1,813 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..requests.connector_selection import ConnectorSelectionParams +from ..requests.connector_setup import ConnectorSetupParams +from ..types.connector import Connector +from ..types.connector_deleted import ConnectorDeleted +from ..types.connector_list import ConnectorList +from ..types.connector_provider import ConnectorProvider +from ..types.connector_setup_result import ConnectorSetupResult +from ..types.connector_sync_started import ConnectorSyncStarted +from .raw_client import AsyncRawConnectorsClient, RawConnectorsClient +from .types.delete_connectors_request_delete_documents import DeleteConnectorsRequestDeleteDocuments + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class ConnectorsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawConnectorsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawConnectorsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawConnectorsClient + """ + return self._raw_client + + def list_providers( + self, + *, + provider: typing.Optional[ConnectorProvider] = None, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> ConnectorList: + """ + List connectors across every namespace this key can read. Use it for admin views that span users or projects. + + Parameters + ---------- + provider : typing.Optional[ConnectorProvider] + Only return connectors for this provider + + page : typing.Optional[int] + One-based page number + + limit : typing.Optional[int] + Maximum connectors to return per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ConnectorList + Connectors across namespaces + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.connectors.list_providers() + """ + _response = self._raw_client.list_providers( + provider=provider, page=page, limit=limit, request_options=request_options + ) + return _response.data + + def list( + self, + namespace: str, + *, + provider: typing.Optional[ConnectorProvider] = None, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> ConnectorList: + """ + List the connectors that sync into this namespace. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + provider : typing.Optional[ConnectorProvider] + Only return connectors for this provider + + page : typing.Optional[int] + One-based page number + + limit : typing.Optional[int] + Maximum connectors to return per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ConnectorList + Connectors in this namespace + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.connectors.list( + namespace="user_alex", + ) + """ + _response = self._raw_client.list( + namespace, provider=provider, page=page, limit=limit, request_options=request_options + ) + return _response.data + + def create( + self, + namespace: str, + *, + request: ConnectorSetupParams, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> ConnectorSetupResult: + """ + Connect an external source to this namespace. OAuth providers return an authUrl to send the user to; providers that authenticate with config start syncing right away. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + request : ConnectorSetupParams + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ConnectorSetupResult + Connector created + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.connectors.create( + namespace="user_alex", + request={"provider": "notion"}, + ) + """ + _response = self._raw_client.create(namespace, request=request, request_options=request_options) + return _response.data + + def get( + self, + namespace: str, + id: str, + *, + attach: typing.Optional[str] = None, + return_url: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> Connector: + """ + Read one connector. Add attach=syncs to include its recent sync runs and the items that failed. attach=picker needs an admin with write access, since the link changes what syncs. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + id : str + Connector identifier returned when the connector was created + + attach : typing.Optional[str] + Comma-separated extras. syncs: the 10 most recent sync runs with their failed items. picker: a one-time hosted picker URL. + + return_url : typing.Optional[str] + Where the hosted picker sends the user when they finish + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Connector + Connector + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.connectors.get( + namespace="user_alex", + id="PTzGiUYei7pgzg5buzZHgA", + ) + """ + _response = self._raw_client.get( + namespace, id, attach=attach, return_url=return_url, request_options=request_options + ) + return _response.data + + def delete( + self, + namespace: str, + id: str, + *, + delete_documents: typing.Optional[DeleteConnectorsRequestDeleteDocuments] = None, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> ConnectorDeleted: + """ + Disconnect a connector and stop its webhooks. Its imported documents are deleted too unless deleteDocuments is false. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + id : str + Connector identifier returned when the connector was created + + delete_documents : typing.Optional[DeleteConnectorsRequestDeleteDocuments] + Also delete documents this connector imported. Defaults to true. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ConnectorDeleted + Connector deleted + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.connectors.delete( + namespace="user_alex", + id="PTzGiUYei7pgzg5buzZHgA", + ) + """ + _response = self._raw_client.delete( + namespace, id, delete_documents=delete_documents, request_options=request_options + ) + return _response.data + + def update( + self, + namespace: str, + id: str, + *, + selection: typing.Optional[ConnectorSelectionParams] = OMIT, + document_limit: typing.Optional[int] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> Connector: + """ + Change what a connector syncs. A new selection replaces the old one and starts a sync. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + id : str + Connector identifier returned when the connector was created + + selection : typing.Optional[ConnectorSelectionParams] + What to sync, as ids grouped by kind. GitHub takes repos, Gmail takes labels, Google Drive takes files and folders. Replaces the current selection. + + document_limit : typing.Optional[int] + Maximum documents this connector imports + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Connector + Updated connector + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.connectors.update( + namespace="user_alex", + id="PTzGiUYei7pgzg5buzZHgA", + ) + """ + _response = self._raw_client.update( + namespace, id, selection=selection, document_limit=document_limit, request_options=request_options + ) + return _response.data + + def sync( + self, + namespace: str, + id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> ConnectorSyncStarted: + """ + Start a sync now. Returns 409 while a sync for this connector is already running. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + id : str + Connector identifier returned when the connector was created + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ConnectorSyncStarted + Sync queued + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.connectors.sync( + namespace="user_alex", + id="PTzGiUYei7pgzg5buzZHgA", + ) + """ + _response = self._raw_client.sync(namespace, id, request_options=request_options) + return _response.data + + +class AsyncConnectorsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawConnectorsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawConnectorsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawConnectorsClient + """ + return self._raw_client + + async def list_providers( + self, + *, + provider: typing.Optional[ConnectorProvider] = None, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> ConnectorList: + """ + List connectors across every namespace this key can read. Use it for admin views that span users or projects. + + Parameters + ---------- + provider : typing.Optional[ConnectorProvider] + Only return connectors for this provider + + page : typing.Optional[int] + One-based page number + + limit : typing.Optional[int] + Maximum connectors to return per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ConnectorList + Connectors across namespaces + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.connectors.list_providers() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list_providers( + provider=provider, page=page, limit=limit, request_options=request_options + ) + return _response.data + + async def list( + self, + namespace: str, + *, + provider: typing.Optional[ConnectorProvider] = None, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> ConnectorList: + """ + List the connectors that sync into this namespace. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + provider : typing.Optional[ConnectorProvider] + Only return connectors for this provider + + page : typing.Optional[int] + One-based page number + + limit : typing.Optional[int] + Maximum connectors to return per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ConnectorList + Connectors in this namespace + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.connectors.list( + namespace="user_alex", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.list( + namespace, provider=provider, page=page, limit=limit, request_options=request_options + ) + return _response.data + + async def create( + self, + namespace: str, + *, + request: ConnectorSetupParams, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> ConnectorSetupResult: + """ + Connect an external source to this namespace. OAuth providers return an authUrl to send the user to; providers that authenticate with config start syncing right away. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + request : ConnectorSetupParams + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ConnectorSetupResult + Connector created + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.connectors.create( + namespace="user_alex", + request={"provider": "notion"}, + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create(namespace, request=request, request_options=request_options) + return _response.data + + async def get( + self, + namespace: str, + id: str, + *, + attach: typing.Optional[str] = None, + return_url: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> Connector: + """ + Read one connector. Add attach=syncs to include its recent sync runs and the items that failed. attach=picker needs an admin with write access, since the link changes what syncs. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + id : str + Connector identifier returned when the connector was created + + attach : typing.Optional[str] + Comma-separated extras. syncs: the 10 most recent sync runs with their failed items. picker: a one-time hosted picker URL. + + return_url : typing.Optional[str] + Where the hosted picker sends the user when they finish + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Connector + Connector + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.connectors.get( + namespace="user_alex", + id="PTzGiUYei7pgzg5buzZHgA", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get( + namespace, id, attach=attach, return_url=return_url, request_options=request_options + ) + return _response.data + + async def delete( + self, + namespace: str, + id: str, + *, + delete_documents: typing.Optional[DeleteConnectorsRequestDeleteDocuments] = None, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> ConnectorDeleted: + """ + Disconnect a connector and stop its webhooks. Its imported documents are deleted too unless deleteDocuments is false. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + id : str + Connector identifier returned when the connector was created + + delete_documents : typing.Optional[DeleteConnectorsRequestDeleteDocuments] + Also delete documents this connector imported. Defaults to true. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ConnectorDeleted + Connector deleted + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.connectors.delete( + namespace="user_alex", + id="PTzGiUYei7pgzg5buzZHgA", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete( + namespace, id, delete_documents=delete_documents, request_options=request_options + ) + return _response.data + + async def update( + self, + namespace: str, + id: str, + *, + selection: typing.Optional[ConnectorSelectionParams] = OMIT, + document_limit: typing.Optional[int] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> Connector: + """ + Change what a connector syncs. A new selection replaces the old one and starts a sync. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + id : str + Connector identifier returned when the connector was created + + selection : typing.Optional[ConnectorSelectionParams] + What to sync, as ids grouped by kind. GitHub takes repos, Gmail takes labels, Google Drive takes files and folders. Replaces the current selection. + + document_limit : typing.Optional[int] + Maximum documents this connector imports + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Connector + Updated connector + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.connectors.update( + namespace="user_alex", + id="PTzGiUYei7pgzg5buzZHgA", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.update( + namespace, id, selection=selection, document_limit=document_limit, request_options=request_options + ) + return _response.data + + async def sync( + self, + namespace: str, + id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> ConnectorSyncStarted: + """ + Start a sync now. Returns 409 while a sync for this connector is already running. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + id : str + Connector identifier returned when the connector was created + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ConnectorSyncStarted + Sync queued + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.connectors.sync( + namespace="user_alex", + id="PTzGiUYei7pgzg5buzZHgA", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.sync(namespace, id, request_options=request_options) + return _response.data diff --git a/src/supermemory/connectors/raw_client.py b/src/supermemory/connectors/raw_client.py new file mode 100644 index 00000000..84929b9c --- /dev/null +++ b/src/supermemory/connectors/raw_client.py @@ -0,0 +1,1748 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import encode_path_param +from ..core.parse_error import ParsingError +from ..core.request_options import RequestOptions +from ..core.serialization import convert_and_respect_annotation_metadata +from ..core.unchecked_base_model import construct_type +from ..errors.bad_request_error import BadRequestError +from ..errors.conflict_error import ConflictError +from ..errors.forbidden_error import ForbiddenError +from ..errors.internal_server_error import InternalServerError +from ..errors.not_found_error import NotFoundError +from ..errors.payment_required_error import PaymentRequiredError +from ..errors.service_unavailable_error import ServiceUnavailableError +from ..errors.unauthorized_error import UnauthorizedError +from ..requests.connector_selection import ConnectorSelectionParams +from ..requests.connector_setup import ConnectorSetupParams +from ..types.bad_request_error_body import BadRequestErrorBody +from ..types.connector import Connector +from ..types.connector_deleted import ConnectorDeleted +from ..types.connector_list import ConnectorList +from ..types.connector_provider import ConnectorProvider +from ..types.connector_setup_result import ConnectorSetupResult +from ..types.connector_sync_started import ConnectorSyncStarted +from ..types.error_response import ErrorResponse +from .types.delete_connectors_request_delete_documents import DeleteConnectorsRequestDeleteDocuments +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawConnectorsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list_providers( + self, + *, + provider: typing.Optional[ConnectorProvider] = None, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[ConnectorList]: + """ + List connectors across every namespace this key can read. Use it for admin views that span users or projects. + + Parameters + ---------- + provider : typing.Optional[ConnectorProvider] + Only return connectors for this provider + + page : typing.Optional[int] + One-based page number + + limit : typing.Optional[int] + Maximum connectors to return per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ConnectorList] + Connectors across namespaces + """ + _response = self._client_wrapper.httpx_client.request( + "connectors", + method="GET", + params={ + "provider": provider, + "page": page, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ConnectorList, + construct_type( + type_=ConnectorList, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def list( + self, + namespace: str, + *, + provider: typing.Optional[ConnectorProvider] = None, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[ConnectorList]: + """ + List the connectors that sync into this namespace. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + provider : typing.Optional[ConnectorProvider] + Only return connectors for this provider + + page : typing.Optional[int] + One-based page number + + limit : typing.Optional[int] + Maximum connectors to return per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ConnectorList] + Connectors in this namespace + """ + _response = self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/connectors", + method="GET", + params={ + "provider": provider, + "page": page, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ConnectorList, + construct_type( + type_=ConnectorList, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def create( + self, + namespace: str, + *, + request: ConnectorSetupParams, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[ConnectorSetupResult]: + """ + Connect an external source to this namespace. OAuth providers return an authUrl to send the user to; providers that authenticate with config start syncing right away. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + request : ConnectorSetupParams + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ConnectorSetupResult] + Connector created + """ + _response = self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/connectors", + method="POST", + json=convert_and_respect_annotation_metadata( + object_=request, annotation=ConnectorSetupParams, direction="write" + ), + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ConnectorSetupResult, + construct_type( + type_=ConnectorSetupResult, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 503: + raise ServiceUnavailableError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get( + self, + namespace: str, + id: str, + *, + attach: typing.Optional[str] = None, + return_url: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[Connector]: + """ + Read one connector. Add attach=syncs to include its recent sync runs and the items that failed. attach=picker needs an admin with write access, since the link changes what syncs. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + id : str + Connector identifier returned when the connector was created + + attach : typing.Optional[str] + Comma-separated extras. syncs: the 10 most recent sync runs with their failed items. picker: a one-time hosted picker URL. + + return_url : typing.Optional[str] + Where the hosted picker sends the user when they finish + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[Connector] + Connector + """ + _response = self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/connectors/{encode_path_param(id)}", + method="GET", + params={ + "attach": attach, + "returnUrl": return_url, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Connector, + construct_type( + type_=Connector, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def delete( + self, + namespace: str, + id: str, + *, + delete_documents: typing.Optional[DeleteConnectorsRequestDeleteDocuments] = None, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[ConnectorDeleted]: + """ + Disconnect a connector and stop its webhooks. Its imported documents are deleted too unless deleteDocuments is false. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + id : str + Connector identifier returned when the connector was created + + delete_documents : typing.Optional[DeleteConnectorsRequestDeleteDocuments] + Also delete documents this connector imported. Defaults to true. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ConnectorDeleted] + Connector deleted + """ + _response = self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/connectors/{encode_path_param(id)}", + method="DELETE", + params={ + "deleteDocuments": delete_documents, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ConnectorDeleted, + construct_type( + type_=ConnectorDeleted, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def update( + self, + namespace: str, + id: str, + *, + selection: typing.Optional[ConnectorSelectionParams] = OMIT, + document_limit: typing.Optional[int] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[Connector]: + """ + Change what a connector syncs. A new selection replaces the old one and starts a sync. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + id : str + Connector identifier returned when the connector was created + + selection : typing.Optional[ConnectorSelectionParams] + What to sync, as ids grouped by kind. GitHub takes repos, Gmail takes labels, Google Drive takes files and folders. Replaces the current selection. + + document_limit : typing.Optional[int] + Maximum documents this connector imports + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[Connector] + Updated connector + """ + _response = self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/connectors/{encode_path_param(id)}", + method="PATCH", + json={ + "selection": convert_and_respect_annotation_metadata( + object_=selection, annotation=ConnectorSelectionParams, direction="write" + ), + "documentLimit": document_limit, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Connector, + construct_type( + type_=Connector, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def sync( + self, + namespace: str, + id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[ConnectorSyncStarted]: + """ + Start a sync now. Returns 409 while a sync for this connector is already running. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + id : str + Connector identifier returned when the connector was created + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ConnectorSyncStarted] + Sync queued + """ + _response = self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/connectors/{encode_path_param(id)}/sync", + method="POST", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ConnectorSyncStarted, + construct_type( + type_=ConnectorSyncStarted, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 503: + raise ServiceUnavailableError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawConnectorsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list_providers( + self, + *, + provider: typing.Optional[ConnectorProvider] = None, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[ConnectorList]: + """ + List connectors across every namespace this key can read. Use it for admin views that span users or projects. + + Parameters + ---------- + provider : typing.Optional[ConnectorProvider] + Only return connectors for this provider + + page : typing.Optional[int] + One-based page number + + limit : typing.Optional[int] + Maximum connectors to return per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ConnectorList] + Connectors across namespaces + """ + _response = await self._client_wrapper.httpx_client.request( + "connectors", + method="GET", + params={ + "provider": provider, + "page": page, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ConnectorList, + construct_type( + type_=ConnectorList, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def list( + self, + namespace: str, + *, + provider: typing.Optional[ConnectorProvider] = None, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[ConnectorList]: + """ + List the connectors that sync into this namespace. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + provider : typing.Optional[ConnectorProvider] + Only return connectors for this provider + + page : typing.Optional[int] + One-based page number + + limit : typing.Optional[int] + Maximum connectors to return per page + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ConnectorList] + Connectors in this namespace + """ + _response = await self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/connectors", + method="GET", + params={ + "provider": provider, + "page": page, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ConnectorList, + construct_type( + type_=ConnectorList, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def create( + self, + namespace: str, + *, + request: ConnectorSetupParams, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[ConnectorSetupResult]: + """ + Connect an external source to this namespace. OAuth providers return an authUrl to send the user to; providers that authenticate with config start syncing right away. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + request : ConnectorSetupParams + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ConnectorSetupResult] + Connector created + """ + _response = await self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/connectors", + method="POST", + json=convert_and_respect_annotation_metadata( + object_=request, annotation=ConnectorSetupParams, direction="write" + ), + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ConnectorSetupResult, + construct_type( + type_=ConnectorSetupResult, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 503: + raise ServiceUnavailableError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get( + self, + namespace: str, + id: str, + *, + attach: typing.Optional[str] = None, + return_url: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[Connector]: + """ + Read one connector. Add attach=syncs to include its recent sync runs and the items that failed. attach=picker needs an admin with write access, since the link changes what syncs. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + id : str + Connector identifier returned when the connector was created + + attach : typing.Optional[str] + Comma-separated extras. syncs: the 10 most recent sync runs with their failed items. picker: a one-time hosted picker URL. + + return_url : typing.Optional[str] + Where the hosted picker sends the user when they finish + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[Connector] + Connector + """ + _response = await self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/connectors/{encode_path_param(id)}", + method="GET", + params={ + "attach": attach, + "returnUrl": return_url, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Connector, + construct_type( + type_=Connector, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def delete( + self, + namespace: str, + id: str, + *, + delete_documents: typing.Optional[DeleteConnectorsRequestDeleteDocuments] = None, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[ConnectorDeleted]: + """ + Disconnect a connector and stop its webhooks. Its imported documents are deleted too unless deleteDocuments is false. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + id : str + Connector identifier returned when the connector was created + + delete_documents : typing.Optional[DeleteConnectorsRequestDeleteDocuments] + Also delete documents this connector imported. Defaults to true. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ConnectorDeleted] + Connector deleted + """ + _response = await self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/connectors/{encode_path_param(id)}", + method="DELETE", + params={ + "deleteDocuments": delete_documents, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ConnectorDeleted, + construct_type( + type_=ConnectorDeleted, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def update( + self, + namespace: str, + id: str, + *, + selection: typing.Optional[ConnectorSelectionParams] = OMIT, + document_limit: typing.Optional[int] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[Connector]: + """ + Change what a connector syncs. A new selection replaces the old one and starts a sync. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + id : str + Connector identifier returned when the connector was created + + selection : typing.Optional[ConnectorSelectionParams] + What to sync, as ids grouped by kind. GitHub takes repos, Gmail takes labels, Google Drive takes files and folders. Replaces the current selection. + + document_limit : typing.Optional[int] + Maximum documents this connector imports + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[Connector] + Updated connector + """ + _response = await self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/connectors/{encode_path_param(id)}", + method="PATCH", + json={ + "selection": convert_and_respect_annotation_metadata( + object_=selection, annotation=ConnectorSelectionParams, direction="write" + ), + "documentLimit": document_limit, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Connector, + construct_type( + type_=Connector, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def sync( + self, + namespace: str, + id: str, + *, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[ConnectorSyncStarted]: + """ + Start a sync now. Returns 409 while a sync for this connector is already running. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + id : str + Connector identifier returned when the connector was created + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ConnectorSyncStarted] + Sync queued + """ + _response = await self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/connectors/{encode_path_param(id)}/sync", + method="POST", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ConnectorSyncStarted, + construct_type( + type_=ConnectorSyncStarted, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 503: + raise ServiceUnavailableError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/supermemory/connectors/types/__init__.py b/src/supermemory/connectors/types/__init__.py new file mode 100644 index 00000000..0963f5a3 --- /dev/null +++ b/src/supermemory/connectors/types/__init__.py @@ -0,0 +1,36 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .delete_connectors_request_delete_documents import DeleteConnectorsRequestDeleteDocuments +_dynamic_imports: typing.Dict[str, str] = { + "DeleteConnectorsRequestDeleteDocuments": ".delete_connectors_request_delete_documents" +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["DeleteConnectorsRequestDeleteDocuments"] diff --git a/src/supermemory/connectors/types/delete_connectors_request_delete_documents.py b/src/supermemory/connectors/types/delete_connectors_request_delete_documents.py new file mode 100644 index 00000000..cbffb5bb --- /dev/null +++ b/src/supermemory/connectors/types/delete_connectors_request_delete_documents.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +DeleteConnectorsRequestDeleteDocuments = typing.Union[typing.Literal["true", "false"], typing.Any] diff --git a/src/supermemory/core/__init__.py b/src/supermemory/core/__init__.py new file mode 100644 index 00000000..4129633e --- /dev/null +++ b/src/supermemory/core/__init__.py @@ -0,0 +1,134 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .api_error import ApiError + from .client_wrapper import AsyncClientWrapper, BaseClientWrapper, SyncClientWrapper + from .datetime_utils import Rfc2822DateTime, parse_rfc2822_datetime, serialize_datetime + from .file import File, convert_file_dict_to_httpx_tuples, with_content_type + from .http_client import AsyncHttpClient, HttpClient + from .http_response import AsyncHttpResponse, HttpResponse + from .jsonable_encoder import encode_path_param, jsonable_encoder + from .logging import ConsoleLogger, ILogger, LogConfig, LogLevel, Logger, create_logger + from .parse_error import ParsingError + from .pydantic_utilities import ( + IS_PYDANTIC_V2, + UniversalBaseModel, + UniversalRootModel, + parse_obj_as, + universal_field_validator, + universal_root_validator, + update_forward_refs, + ) + from .query_encoder import encode_query + from .remove_none_from_dict import remove_none_from_dict + from .request_options import RequestOptions + from .serialization import FieldMetadata, convert_and_respect_annotation_metadata + from .unchecked_base_model import UncheckedBaseModel, UnionMetadata, construct_type +_dynamic_imports: typing.Dict[str, str] = { + "ApiError": ".api_error", + "AsyncClientWrapper": ".client_wrapper", + "AsyncHttpClient": ".http_client", + "AsyncHttpResponse": ".http_response", + "BaseClientWrapper": ".client_wrapper", + "ConsoleLogger": ".logging", + "FieldMetadata": ".serialization", + "File": ".file", + "HttpClient": ".http_client", + "HttpResponse": ".http_response", + "ILogger": ".logging", + "IS_PYDANTIC_V2": ".pydantic_utilities", + "LogConfig": ".logging", + "LogLevel": ".logging", + "Logger": ".logging", + "ParsingError": ".parse_error", + "RequestOptions": ".request_options", + "Rfc2822DateTime": ".datetime_utils", + "SyncClientWrapper": ".client_wrapper", + "UncheckedBaseModel": ".unchecked_base_model", + "UnionMetadata": ".unchecked_base_model", + "UniversalBaseModel": ".pydantic_utilities", + "UniversalRootModel": ".pydantic_utilities", + "construct_type": ".unchecked_base_model", + "convert_and_respect_annotation_metadata": ".serialization", + "convert_file_dict_to_httpx_tuples": ".file", + "create_logger": ".logging", + "encode_path_param": ".jsonable_encoder", + "encode_query": ".query_encoder", + "jsonable_encoder": ".jsonable_encoder", + "parse_obj_as": ".pydantic_utilities", + "parse_rfc2822_datetime": ".datetime_utils", + "remove_none_from_dict": ".remove_none_from_dict", + "serialize_datetime": ".datetime_utils", + "universal_field_validator": ".pydantic_utilities", + "universal_root_validator": ".pydantic_utilities", + "update_forward_refs": ".pydantic_utilities", + "with_content_type": ".file", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "ApiError", + "AsyncClientWrapper", + "AsyncHttpClient", + "AsyncHttpResponse", + "BaseClientWrapper", + "ConsoleLogger", + "FieldMetadata", + "File", + "HttpClient", + "HttpResponse", + "ILogger", + "IS_PYDANTIC_V2", + "LogConfig", + "LogLevel", + "Logger", + "ParsingError", + "RequestOptions", + "Rfc2822DateTime", + "SyncClientWrapper", + "UncheckedBaseModel", + "UnionMetadata", + "UniversalBaseModel", + "UniversalRootModel", + "construct_type", + "convert_and_respect_annotation_metadata", + "convert_file_dict_to_httpx_tuples", + "create_logger", + "encode_path_param", + "encode_query", + "jsonable_encoder", + "parse_obj_as", + "parse_rfc2822_datetime", + "remove_none_from_dict", + "serialize_datetime", + "universal_field_validator", + "universal_root_validator", + "update_forward_refs", + "with_content_type", +] diff --git a/src/supermemory/core/api_error.py b/src/supermemory/core/api_error.py new file mode 100644 index 00000000..6f850a60 --- /dev/null +++ b/src/supermemory/core/api_error.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, Optional + + +class ApiError(Exception): + headers: Optional[Dict[str, str]] + status_code: Optional[int] + body: Any + + def __init__( + self, + *, + headers: Optional[Dict[str, str]] = None, + status_code: Optional[int] = None, + body: Any = None, + ) -> None: + self.headers = headers + self.status_code = status_code + self.body = body + + def __str__(self) -> str: + return f"headers: {self.headers}, status_code: {self.status_code}, body: {self.body}" diff --git a/src/supermemory/core/client_wrapper.py b/src/supermemory/core/client_wrapper.py new file mode 100644 index 00000000..1955eb68 --- /dev/null +++ b/src/supermemory/core/client_wrapper.py @@ -0,0 +1,145 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx +from .http_client import AsyncHttpClient, HttpClient +from .logging import LogConfig, Logger + + +class BaseClientWrapper: + def __init__( + self, + *, + api_key: typing.Union[str, typing.Callable[[], str]], + headers: typing.Optional[typing.Dict[str, str]] = None, + base_url: str, + timeout: typing.Optional[float] = None, + max_retries: int = 2, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, + ): + self._api_key = api_key + self._headers = headers + self._base_url = base_url + self._timeout = timeout + self._max_retries = max_retries + self._stream_reconnection_enabled = stream_reconnection_enabled + self._max_stream_reconnection_attempts = max_stream_reconnection_attempts + self._logging = logging + + def get_headers(self) -> typing.Dict[str, str]: + import platform + + headers: typing.Dict[str, str] = { + "User-Agent": "supermemory/5.0.0rc1", + "X-Fern-Language": "Python", + "X-Fern-Runtime": f"python/{platform.python_version()}", + "X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}", + **(self.get_custom_headers() or {}), + } + headers["Authorization"] = f"Bearer {self._get_api_key()}" + return headers + + def _get_api_key(self) -> str: + if isinstance(self._api_key, str): + return self._api_key + else: + return self._api_key() + + def get_custom_headers(self) -> typing.Optional[typing.Dict[str, str]]: + return self._headers + + def get_base_url(self) -> str: + return self._base_url + + def get_timeout(self) -> typing.Optional[float]: + return self._timeout + + def get_max_retries(self) -> int: + return self._max_retries + + def get_stream_reconnection_enabled(self) -> bool: + return self._stream_reconnection_enabled if self._stream_reconnection_enabled is not None else True + + def get_max_stream_reconnection_attempts(self) -> typing.Optional[int]: + return self._max_stream_reconnection_attempts + + +class SyncClientWrapper(BaseClientWrapper): + def __init__( + self, + *, + api_key: typing.Union[str, typing.Callable[[], str]], + headers: typing.Optional[typing.Dict[str, str]] = None, + base_url: str, + timeout: typing.Optional[float] = None, + max_retries: int = 2, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, + httpx_client: httpx.Client, + ): + super().__init__( + api_key=api_key, + headers=headers, + base_url=base_url, + timeout=timeout, + max_retries=max_retries, + stream_reconnection_enabled=stream_reconnection_enabled, + max_stream_reconnection_attempts=max_stream_reconnection_attempts, + logging=logging, + ) + self.httpx_client = HttpClient( + httpx_client=httpx_client, + base_headers=self.get_headers, + base_timeout=self.get_timeout, + base_url=self.get_base_url, + base_max_retries=self.get_max_retries(), + logging_config=self._logging, + ) + + +class AsyncClientWrapper(BaseClientWrapper): + def __init__( + self, + *, + api_key: typing.Union[str, typing.Callable[[], str]], + headers: typing.Optional[typing.Dict[str, str]] = None, + base_url: str, + timeout: typing.Optional[float] = None, + max_retries: int = 2, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, + async_token: typing.Optional[typing.Callable[[], typing.Awaitable[str]]] = None, + httpx_client: httpx.AsyncClient, + ): + super().__init__( + api_key=api_key, + headers=headers, + base_url=base_url, + timeout=timeout, + max_retries=max_retries, + stream_reconnection_enabled=stream_reconnection_enabled, + max_stream_reconnection_attempts=max_stream_reconnection_attempts, + logging=logging, + ) + self._async_token = async_token + self.httpx_client = AsyncHttpClient( + httpx_client=httpx_client, + base_headers=self.get_headers, + base_timeout=self.get_timeout, + base_url=self.get_base_url, + base_max_retries=self.get_max_retries(), + async_base_headers=self.async_get_headers, + logging_config=self._logging, + ) + + async def async_get_headers(self) -> typing.Dict[str, str]: + headers = self.get_headers() + if self._async_token is not None: + token = await self._async_token() + headers["Authorization"] = f"Bearer {token}" + return headers diff --git a/src/supermemory/core/datetime_utils.py b/src/supermemory/core/datetime_utils.py new file mode 100644 index 00000000..a12b2ad0 --- /dev/null +++ b/src/supermemory/core/datetime_utils.py @@ -0,0 +1,70 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +from email.utils import parsedate_to_datetime +from typing import Any + +import pydantic + +IS_PYDANTIC_V2 = pydantic.VERSION.startswith("2.") + + +def parse_rfc2822_datetime(v: Any) -> dt.datetime: + """ + Parse an RFC 2822 datetime string (e.g., "Wed, 02 Oct 2002 13:00:00 GMT") + into a datetime object. If the value is already a datetime, return it as-is. + Falls back to ISO 8601 parsing if RFC 2822 parsing fails. + """ + if isinstance(v, dt.datetime): + return v + if isinstance(v, str): + try: + return parsedate_to_datetime(v) + except Exception: + pass + # Fallback to ISO 8601 parsing + return dt.datetime.fromisoformat(v.replace("Z", "+00:00")) + raise ValueError(f"Expected str or datetime, got {type(v)}") + + +class Rfc2822DateTime(dt.datetime): + """A datetime subclass that parses RFC 2822 date strings. + + On Pydantic V1, uses __get_validators__ for pre-validation. + On Pydantic V2, uses __get_pydantic_core_schema__ for BeforeValidator-style parsing. + """ + + @classmethod + def __get_validators__(cls): # type: ignore[no-untyped-def] + yield parse_rfc2822_datetime + + @classmethod + def __get_pydantic_core_schema__(cls, _source_type: Any, _handler: Any) -> Any: # type: ignore[override] + from pydantic_core import core_schema + + return core_schema.no_info_before_validator_function(parse_rfc2822_datetime, core_schema.datetime_schema()) + + +def serialize_datetime(v: dt.datetime) -> str: + """ + Serialize a datetime including timezone info. + + Uses the timezone info provided if present, otherwise uses the current runtime's timezone info. + + UTC datetimes end in "Z" while all other timezones are represented as offset from UTC, e.g. +05:00. + """ + + def _serialize_zoned_datetime(v: dt.datetime) -> str: + if v.tzinfo is not None and v.tzinfo.tzname(None) == dt.timezone.utc.tzname(None): + # UTC is a special case where we use "Z" at the end instead of "+00:00" + return v.isoformat().replace("+00:00", "Z") + else: + # Delegate to the typical +/- offset format + return v.isoformat() + + if v.tzinfo is not None: + return _serialize_zoned_datetime(v) + else: + local_tz = dt.datetime.now().astimezone().tzinfo + localized_dt = v.replace(tzinfo=local_tz) + return _serialize_zoned_datetime(localized_dt) diff --git a/src/supermemory/core/file.py b/src/supermemory/core/file.py new file mode 100644 index 00000000..44b0d27c --- /dev/null +++ b/src/supermemory/core/file.py @@ -0,0 +1,67 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import IO, Dict, List, Mapping, Optional, Tuple, Union, cast + +# File typing inspired by the flexibility of types within the httpx library +# https://github.com/encode/httpx/blob/master/httpx/_types.py +FileContent = Union[IO[bytes], bytes, str] +File = Union[ + # file (or bytes) + FileContent, + # (filename, file (or bytes)) + Tuple[Optional[str], FileContent], + # (filename, file (or bytes), content_type) + Tuple[Optional[str], FileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + Tuple[ + Optional[str], + FileContent, + Optional[str], + Mapping[str, str], + ], +] + + +def convert_file_dict_to_httpx_tuples( + d: Dict[str, Union[File, List[File]]], +) -> List[Tuple[str, File]]: + """ + The format we use is a list of tuples, where the first element is the + name of the file and the second is the file object. Typically HTTPX wants + a dict, but to be able to send lists of files, you have to use the list + approach (which also works for non-lists) + https://github.com/encode/httpx/pull/1032 + """ + + httpx_tuples = [] + for key, file_like in d.items(): + if isinstance(file_like, list): + for file_like_item in file_like: + httpx_tuples.append((key, file_like_item)) + else: + httpx_tuples.append((key, file_like)) + return httpx_tuples + + +def with_content_type(*, file: File, default_content_type: str) -> File: + """ + This function resolves to the file's content type, if provided, and defaults + to the default_content_type value if not. + """ + if isinstance(file, tuple): + if len(file) == 2: + filename, content = cast(Tuple[Optional[str], FileContent], file) # type: ignore + return (filename, content, default_content_type) + elif len(file) == 3: + filename, content, file_content_type = cast(Tuple[Optional[str], FileContent, Optional[str]], file) # type: ignore + out_content_type = file_content_type or default_content_type + return (filename, content, out_content_type) + elif len(file) == 4: + filename, content, file_content_type, headers = cast( # type: ignore + Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], file + ) + out_content_type = file_content_type or default_content_type + return (filename, content, out_content_type, headers) + else: + raise ValueError(f"Unexpected tuple length: {len(file)}") + return (None, file, default_content_type) diff --git a/src/supermemory/core/force_multipart.py b/src/supermemory/core/force_multipart.py new file mode 100644 index 00000000..5440913f --- /dev/null +++ b/src/supermemory/core/force_multipart.py @@ -0,0 +1,18 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict + + +class ForceMultipartDict(Dict[str, Any]): + """ + A dictionary subclass that always evaluates to True in boolean contexts. + + This is used to force multipart/form-data encoding in HTTP requests even when + the dictionary is empty, which would normally evaluate to False. + """ + + def __bool__(self) -> bool: + return True + + +FORCE_MULTIPART = ForceMultipartDict() diff --git a/src/supermemory/core/http_client.py b/src/supermemory/core/http_client.py new file mode 100644 index 00000000..124dce25 --- /dev/null +++ b/src/supermemory/core/http_client.py @@ -0,0 +1,851 @@ +# This file was auto-generated by Fern from our API Definition. + +import asyncio +import email.utils +import re +import time +import typing +from contextlib import asynccontextmanager, contextmanager +from random import random + +import httpx +from .file import File, convert_file_dict_to_httpx_tuples +from .force_multipart import FORCE_MULTIPART +from .jsonable_encoder import jsonable_encoder +from .logging import LogConfig, Logger, create_logger +from .query_encoder import encode_query +from .remove_none_from_dict import remove_none_from_dict as remove_none_from_dict +from .request_options import RequestOptions +from httpx._types import RequestFiles + +INITIAL_RETRY_DELAY_SECONDS = 1.0 +MAX_RETRY_DELAY_SECONDS = 60.0 +JITTER_FACTOR = 0.2 # 20% random jitter + + +def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]: + """ + This function parses the `Retry-After` header in a HTTP response and returns the number of seconds to wait. + + Inspired by the urllib3 retry implementation. + """ + retry_after_ms = response_headers.get("retry-after-ms") + if retry_after_ms is not None: + try: + return int(retry_after_ms) / 1000 if retry_after_ms > 0 else 0 + except Exception: + pass + + retry_after = response_headers.get("retry-after") + if retry_after is None: + return None + + # Attempt to parse the header as an int. + if re.match(r"^\s*[0-9]+\s*$", retry_after): + seconds = float(retry_after) + # Fallback to parsing it as a date. + else: + retry_date_tuple = email.utils.parsedate_tz(retry_after) + if retry_date_tuple is None: + return None + if retry_date_tuple[9] is None: # Python 2 + # Assume UTC if no timezone was specified + # On Python2.7, parsedate_tz returns None for a timezone offset + # instead of 0 if no timezone is given, where mktime_tz treats + # a None timezone offset as local time. + retry_date_tuple = retry_date_tuple[:9] + (0,) + retry_date_tuple[10:] + + retry_date = email.utils.mktime_tz(retry_date_tuple) + seconds = retry_date - time.time() + + if seconds < 0: + seconds = 0 + + return seconds + + +def _add_positive_jitter(delay: float) -> float: + """Add positive jitter (0-20%) to prevent thundering herd.""" + jitter_multiplier = 1 + random() * JITTER_FACTOR + return delay * jitter_multiplier + + +def _add_symmetric_jitter(delay: float) -> float: + """Add symmetric jitter (±10%) for exponential backoff.""" + jitter_multiplier = 1 + (random() - 0.5) * JITTER_FACTOR + return delay * jitter_multiplier + + +def _parse_x_ratelimit_reset(response_headers: httpx.Headers) -> typing.Optional[float]: + """ + Parse the X-RateLimit-Reset header (Unix timestamp in seconds). + Returns seconds to wait, or None if header is missing/invalid. + """ + reset_time_str = response_headers.get("x-ratelimit-reset") + if reset_time_str is None: + return None + + try: + reset_time = int(reset_time_str) + delay = reset_time - time.time() + if delay > 0: + return delay + except (ValueError, TypeError): + pass + + return None + + +def _retry_timeout(response: httpx.Response, retries: int) -> float: + """ + Determine the amount of time to wait before retrying a request. + This function begins by trying to parse a retry-after header from the response, and then proceeds to use exponential backoff + with a jitter to determine the number of seconds to wait. + """ + + # 1. Check Retry-After header first + retry_after = _parse_retry_after(response.headers) + if retry_after is not None and retry_after > 0: + return min(retry_after, MAX_RETRY_DELAY_SECONDS) + + # 2. Check X-RateLimit-Reset header (with positive jitter) + ratelimit_reset = _parse_x_ratelimit_reset(response.headers) + if ratelimit_reset is not None: + return _add_positive_jitter(min(ratelimit_reset, MAX_RETRY_DELAY_SECONDS)) + + # 3. Fall back to exponential backoff (with symmetric jitter) + backoff = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS) + return _add_symmetric_jitter(backoff) + + +def _retry_timeout_from_retries(retries: int) -> float: + """Determine retry timeout using exponential backoff when no response is available.""" + backoff = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS) + return _add_symmetric_jitter(backoff) + + +def _should_retry(response: httpx.Response) -> bool: + return response.status_code >= 500 or response.status_code in [429, 408, 409] + + +_SENSITIVE_HEADERS = frozenset( + { + "authorization", + "www-authenticate", + "x-api-key", + "api-key", + "apikey", + "x-api-token", + "x-auth-token", + "auth-token", + "cookie", + "set-cookie", + "proxy-authorization", + "proxy-authenticate", + "x-csrf-token", + "x-xsrf-token", + "x-session-token", + "x-access-token", + } +) + + +def _redact_headers(headers: typing.Dict[str, str]) -> typing.Dict[str, str]: + return {k: ("[REDACTED]" if k.lower() in _SENSITIVE_HEADERS else v) for k, v in headers.items()} + + +def _build_url(base_url: str, path: typing.Optional[str]) -> str: + """ + Build a full URL by joining a base URL with a path. + + This function correctly handles base URLs that contain path prefixes (e.g., tenant-based URLs) + by using string concatenation instead of urllib.parse.urljoin(), which would incorrectly + strip path components when the path starts with '/'. + + Example: + >>> _build_url("https://cloud.example.com/org/tenant/api", "/users") + 'https://cloud.example.com/org/tenant/api/users' + + Args: + base_url: The base URL, which may contain path prefixes. + path: The path to append. Can be None or empty string. + + Returns: + The full URL with base_url and path properly joined. + """ + if not path: + return base_url + return f"{base_url.rstrip('/')}/{path.lstrip('/')}" + + +def _maybe_filter_none_from_multipart_data( + data: typing.Optional[typing.Any], + request_files: typing.Optional[RequestFiles], + force_multipart: typing.Optional[bool], +) -> typing.Optional[typing.Any]: + """ + Filter None values from data body for multipart/form requests. + This prevents httpx from converting None to empty strings in multipart encoding. + Only applies when files are present or force_multipart is True. + """ + if data is not None and isinstance(data, typing.Mapping) and (request_files or force_multipart): + return remove_none_from_dict(data) + return data + + +def remove_omit_from_dict( + original: typing.Dict[str, typing.Optional[typing.Any]], + omit: typing.Optional[typing.Any], +) -> typing.Dict[str, typing.Any]: + if omit is None: + return original + new: typing.Dict[str, typing.Any] = {} + for key, value in original.items(): + if value is not omit: + new[key] = value + return new + + +def maybe_filter_request_body( + data: typing.Optional[typing.Any], + request_options: typing.Optional[RequestOptions], + omit: typing.Optional[typing.Any], +) -> typing.Optional[typing.Any]: + if data is None: + return ( + jsonable_encoder(request_options.get("additional_body_parameters", {})) or {} + if request_options is not None + else None + ) + elif not isinstance(data, typing.Mapping): + data_content = jsonable_encoder(data) + else: + data_content = { + **(jsonable_encoder(remove_omit_from_dict(data, omit))), # type: ignore + **( + jsonable_encoder(request_options.get("additional_body_parameters", {})) or {} + if request_options is not None + else {} + ), + } + return data_content + + +# Abstracted out for testing purposes +def get_request_body( + *, + json: typing.Optional[typing.Any], + data: typing.Optional[typing.Any], + request_options: typing.Optional[RequestOptions], + omit: typing.Optional[typing.Any], +) -> typing.Tuple[typing.Optional[typing.Any], typing.Optional[typing.Any]]: + json_body = None + data_body = None + if data is not None: + data_body = maybe_filter_request_body(data, request_options, omit) + else: + # If both data and json are None, we send json data in the event extra properties are specified + json_body = maybe_filter_request_body(json, request_options, omit) + + has_additional_body_parameters = bool( + request_options is not None and request_options.get("additional_body_parameters") + ) + + # Only collapse empty dict to None when the body was not explicitly provided + # and there are no additional body parameters. This preserves explicit empty + # bodies (e.g., when an endpoint has a request body type but all fields are optional). + if json_body == {} and json is None and not has_additional_body_parameters: + json_body = None + if data_body == {} and data is None and not has_additional_body_parameters: + data_body = None + + return json_body, data_body + + +class HttpClient: + def __init__( + self, + *, + httpx_client: httpx.Client, + base_timeout: typing.Callable[[], typing.Optional[float]], + base_headers: typing.Callable[[], typing.Dict[str, str]], + base_url: typing.Optional[typing.Callable[[], str]] = None, + base_max_retries: int = 2, + logging_config: typing.Optional[typing.Union[LogConfig, Logger]] = None, + ): + self.base_url = base_url + self.base_timeout = base_timeout + self.base_headers = base_headers + self.base_max_retries = base_max_retries + self.httpx_client = httpx_client + self.logger = create_logger(logging_config) + + def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: + base_url = maybe_base_url + if self.base_url is not None and base_url is None: + base_url = self.base_url() + + if base_url is None: + raise ValueError("A base_url is required to make this request, please provide one and try again.") + return base_url + + def request( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 0, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> httpx.Response: + base_url = self.get_base_url(base_url) + _timeout = ( + request_options.get("timeout") + if request_options is not None and request_options.get("timeout") is not None + else request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) or {} + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ) + + _request_url = _build_url(base_url, path) + _request_headers = jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers(), + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), + } + ) + ) + + if self.logger.is_debug(): + self.logger.debug( + "Making HTTP request", + method=method, + url=_request_url, + headers=_redact_headers(_request_headers), + has_body=json_body is not None or data_body is not None, + ) + + max_retries: int = ( + request_options.get("max_retries", self.base_max_retries) + if request_options is not None + else self.base_max_retries + ) + + try: + response = self.httpx_client.request( + method=method, + url=_request_url, + headers=_request_headers, + params=_encoded_params if _encoded_params else None, + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) + except (httpx.ConnectError, httpx.RemoteProtocolError): + if retries < max_retries: + time.sleep(_retry_timeout_from_retries(retries=retries)) + return self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + data=data, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + force_multipart=force_multipart, + ) + raise + + if _should_retry(response=response): + if retries < max_retries: + time.sleep(_retry_timeout(response=response, retries=retries)) + return self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + data=data, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + force_multipart=force_multipart, + ) + + if self.logger.is_debug(): + if 200 <= response.status_code < 400: + self.logger.debug( + "HTTP request succeeded", + method=method, + url=_request_url, + status_code=response.status_code, + ) + + if self.logger.is_error(): + if response.status_code >= 400: + self.logger.error( + "HTTP request failed with error status", + method=method, + url=_request_url, + status_code=response.status_code, + ) + + return response + + @contextmanager + def stream( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 0, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> typing.Iterator[httpx.Response]: + base_url = self.get_base_url(base_url) + _timeout = ( + request_options.get("timeout") + if request_options is not None and request_options.get("timeout") is not None + else request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ) + + _request_url = _build_url(base_url, path) + _request_headers = jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers(), + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ) + + if self.logger.is_debug(): + self.logger.debug( + "Making streaming HTTP request", + method=method, + url=_request_url, + headers=_redact_headers(_request_headers), + ) + + with self.httpx_client.stream( + method=method, + url=_request_url, + headers=_request_headers, + params=_encoded_params if _encoded_params else None, + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) as stream: + yield stream + + +class AsyncHttpClient: + def __init__( + self, + *, + httpx_client: httpx.AsyncClient, + base_timeout: typing.Callable[[], typing.Optional[float]], + base_headers: typing.Callable[[], typing.Dict[str, str]], + base_url: typing.Optional[typing.Callable[[], str]] = None, + base_max_retries: int = 2, + async_base_headers: typing.Optional[typing.Callable[[], typing.Awaitable[typing.Dict[str, str]]]] = None, + logging_config: typing.Optional[typing.Union[LogConfig, Logger]] = None, + ): + self.base_url = base_url + self.base_timeout = base_timeout + self.base_headers = base_headers + self.base_max_retries = base_max_retries + self.async_base_headers = async_base_headers + self.httpx_client = httpx_client + self.logger = create_logger(logging_config) + + async def _get_headers(self) -> typing.Dict[str, str]: + if self.async_base_headers is not None: + return await self.async_base_headers() + return self.base_headers() + + def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: + base_url = maybe_base_url + if self.base_url is not None and base_url is None: + base_url = self.base_url() + + if base_url is None: + raise ValueError("A base_url is required to make this request, please provide one and try again.") + return base_url + + async def request( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 0, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> httpx.Response: + base_url = self.get_base_url(base_url) + _timeout = ( + request_options.get("timeout") + if request_options is not None and request_options.get("timeout") is not None + else request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) + + # Get headers (supports async token providers) + _headers = await self._get_headers() + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) or {} + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ) + + _request_url = _build_url(base_url, path) + _request_headers = jsonable_encoder( + remove_none_from_dict( + { + **_headers, + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), + } + ) + ) + + if self.logger.is_debug(): + self.logger.debug( + "Making HTTP request", + method=method, + url=_request_url, + headers=_redact_headers(_request_headers), + has_body=json_body is not None or data_body is not None, + ) + + max_retries: int = ( + request_options.get("max_retries", self.base_max_retries) + if request_options is not None + else self.base_max_retries + ) + + try: + response = await self.httpx_client.request( + method=method, + url=_request_url, + headers=_request_headers, + params=_encoded_params if _encoded_params else None, + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) + except (httpx.ConnectError, httpx.RemoteProtocolError): + if retries < max_retries: + await asyncio.sleep(_retry_timeout_from_retries(retries=retries)) + return await self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + data=data, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + force_multipart=force_multipart, + ) + raise + + if _should_retry(response=response): + if retries < max_retries: + await asyncio.sleep(_retry_timeout(response=response, retries=retries)) + return await self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + data=data, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + force_multipart=force_multipart, + ) + + if self.logger.is_debug(): + if 200 <= response.status_code < 400: + self.logger.debug( + "HTTP request succeeded", + method=method, + url=_request_url, + status_code=response.status_code, + ) + + if self.logger.is_error(): + if response.status_code >= 400: + self.logger.error( + "HTTP request failed with error status", + method=method, + url=_request_url, + status_code=response.status_code, + ) + + return response + + @asynccontextmanager + async def stream( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 0, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> typing.AsyncIterator[httpx.Response]: + base_url = self.get_base_url(base_url) + _timeout = ( + request_options.get("timeout") + if request_options is not None and request_options.get("timeout") is not None + else request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) + + # Get headers (supports async token providers) + _headers = await self._get_headers() + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + if request_options is not None + else {} + ), + }, + omit=omit, + ) + ) + ) + ) + + _request_url = _build_url(base_url, path) + _request_headers = jsonable_encoder( + remove_none_from_dict( + { + **_headers, + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ) + + if self.logger.is_debug(): + self.logger.debug( + "Making streaming HTTP request", + method=method, + url=_request_url, + headers=_redact_headers(_request_headers), + ) + + async with self.httpx_client.stream( + method=method, + url=_request_url, + headers=_request_headers, + params=_encoded_params if _encoded_params else None, + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) as stream: + yield stream diff --git a/src/supermemory/core/http_response.py b/src/supermemory/core/http_response.py new file mode 100644 index 00000000..90272bad --- /dev/null +++ b/src/supermemory/core/http_response.py @@ -0,0 +1,72 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Dict, Generic, TypeVar + +import httpx + +# Generic to represent the underlying type of the data wrapped by the HTTP response. +T = TypeVar("T") + + +class BaseHttpResponse: + """Minimalist HTTP response wrapper that exposes response headers and status code.""" + + _response: httpx.Response + + def __init__(self, response: httpx.Response): + self._response = response + + @property + def headers(self) -> Dict[str, str]: + return dict(self._response.headers) + + @property + def status_code(self) -> int: + return self._response.status_code + + @property + def http_response(self) -> httpx.Response: + """The underlying httpx response (3.x-compatible).""" + return self._response + + +class HttpResponse(Generic[T], BaseHttpResponse): + """HTTP response wrapper that exposes response headers and data.""" + + _data: T + + def __init__(self, response: httpx.Response, data: T): + super().__init__(response) + self._data = data + + @property + def data(self) -> T: + return self._data + + def parse(self) -> T: + """The parsed response body (3.x-compatible alias of `.data`).""" + return self._data + + def close(self) -> None: + self._response.close() + + +class AsyncHttpResponse(Generic[T], BaseHttpResponse): + """HTTP response wrapper that exposes response headers and data.""" + + _data: T + + def __init__(self, response: httpx.Response, data: T): + super().__init__(response) + self._data = data + + @property + def data(self) -> T: + return self._data + + async def parse(self) -> T: + """The parsed response body (3.x-compatible alias of `.data`).""" + return self._data + + async def close(self) -> None: + await self._response.aclose() diff --git a/src/supermemory/core/http_sse/__init__.py b/src/supermemory/core/http_sse/__init__.py new file mode 100644 index 00000000..730e5a33 --- /dev/null +++ b/src/supermemory/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/src/supermemory/core/http_sse/_api.py b/src/supermemory/core/http_sse/_api.py new file mode 100644 index 00000000..9ca5602c --- /dev/null +++ b/src/supermemory/core/http_sse/_api.py @@ -0,0 +1,455 @@ +# This file was auto-generated by Fern from our API Definition. + +import codecs +import re +import time +from contextlib import asynccontextmanager, contextmanager +from typing import ( + Any, + AsyncContextManager, + AsyncGenerator, + AsyncIterator, + Callable, + ContextManager, + Iterator, + Optional, +) + +import anyio +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + +MAX_LINE_SIZE: int = 1_048_576 # 1 MiB + +# Reconnection defaults, mirroring the TypeScript SDK's Stream implementation. +DEFAULT_MAX_RECONNECTION_ATTEMPTS: int = 5 +DEFAULT_RECONNECT_DELAY_MS: int = 1_000 +MAX_RECONNECT_DELAY_MS: int = 30_000 + + +# A reconnect callback re-issues the original request (with a ``Last-Event-ID`` +# header set to the supplied event id) and returns a *context manager* yielding +# a fresh streaming ``httpx.Response``. Sync clients supply a sync context +# manager; async clients supply an async one. +class EventSource: + def __init__( + self, + response: httpx.Response, + *, + resumable: bool = False, + stream_reconnection_enabled: bool = True, + max_stream_reconnection_attempts: Optional[int] = None, + stream_terminator: Optional[str] = None, + reconnect: Optional[Callable[[str], Any]] = None, + ) -> None: + self._response = response + self._resumable = resumable + self._stream_reconnection_enabled = stream_reconnection_enabled + self._max_stream_reconnection_attempts = max_stream_reconnection_attempts + self._stream_terminator = stream_terminator + self._reconnect = reconnect + + @staticmethod + def _is_event_stream(response: httpx.Response) -> bool: + content_type = response.headers.get("content-type", "").partition(";")[0] + return "text/event-stream" in content_type + + def _check_content_type(self) -> None: + if not self._is_event_stream(self._response): + content_type = self._response.headers.get("content-type", "").partition(";")[0] + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _is_reconnect_response_usable(self, response: httpx.Response) -> bool: + """Whether a reconnected response can be resumed as an SSE stream. + + ``httpx.stream`` does not raise on non-success status, so a resume that + returns an error page (e.g. ``200 text/html`` or a ``500`` body) would + otherwise be parsed as SSE and yield garbage/zero events. Such a + response is treated as a failed attempt (back off and retry) instead. + """ + return response.status_code < 400 and self._is_event_stream(response) + + def _get_charset(self, response: Optional[httpx.Response] = None) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + resolved = response if response is not None else self._response + content_type = resolved.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + @staticmethod + def _normalize_sse_line_endings(buf: str) -> str: + """Normalize line endings per the SSE spec (\\r\\n → \\n, bare \\r → \\n). + + A trailing \\r is preserved because it may pair with a leading \\n in + the next chunk to form a single \\r\\n terminator. + """ + buf = buf.replace("\r\n", "\n") + if buf.endswith("\r"): + return buf[:-1].replace("\r", "\n") + "\r" + return buf.replace("\r", "\n") + + def _new_text_decoder(self, response: Optional[httpx.Response] = None) -> "codecs.IncrementalDecoder": + return codecs.getincrementaldecoder(self._get_charset(response))(errors="replace") + + def _reconnect_applicable(self) -> bool: + """Whether reconnection is configured for this stream at all. + + This is the terminator-gating half of the reconnect decision, kept + separate from :meth:`_should_reconnect` (which additionally requires a + last *dispatched* id and an unexhausted attempt budget). The split lets + a mid-stream transport error terminate consistently: + - a stream that can never reconnect (non-resumable, no terminator, + disabled, or no callback) must re-raise the error to the caller, so a + truncated stream is not mistaken for a clean completion; + - a resumable stream that has merely run out of attempts (or has no id + to resume from) ends cleanly — the same way an exhausted empty/error + -body resume already does, matching the TypeScript ``return``. + """ + return ( + self._resumable + and self._stream_terminator is not None + and self._stream_reconnection_enabled + and self._reconnect is not None + ) + + def _should_reconnect(self, last_dispatched_id: Optional[str], reconnect_attempts: int) -> bool: + """Decide whether a prematurely-ended stream should be reconnected. + + Mirrors the TypeScript ``shouldReconnect`` gating: + - only resumable SSE endpoints with a configured terminator, reconnect + enabled, and a reconnect callback are eligible (see + :meth:`_reconnect_applicable`); + - a last *dispatched* event id must exist to resume from; + - the consecutive-failed-attempt cap must not be exceeded. + """ + if not self._reconnect_applicable(): + return False + if not last_dispatched_id: + return False + max_attempts = ( + self._max_stream_reconnection_attempts + if self._max_stream_reconnection_attempts is not None + else DEFAULT_MAX_RECONNECTION_ATTEMPTS + ) + if reconnect_attempts >= max_attempts: + return False + return True + + def _reconnect_delay_seconds(self, last_retry: Optional[int]) -> float: + """Backoff before a reconnect. + + Uses the server's most recent ``retry:`` directive (milliseconds) when + present, otherwise a default of ``DEFAULT_RECONNECT_DELAY_MS``, clamped + to ``MAX_RECONNECT_DELAY_MS``. + """ + base_ms = last_retry if (last_retry is not None and last_retry > 0) else DEFAULT_RECONNECT_DELAY_MS + return min(base_ms, MAX_RECONNECT_DELAY_MS) / 1000.0 + + def _sleep_before_reconnect(self, last_retry: Optional[int]) -> None: + # ``time.sleep`` blocks the calling thread but remains interruptible by + # signals (e.g. ``KeyboardInterrupt``), which propagate out and abort + # the reconnect without issuing another request. + time.sleep(self._reconnect_delay_seconds(last_retry)) + + async def _asleep_before_reconnect(self, last_retry: Optional[int]) -> None: + # ``anyio.sleep`` is cancellation-aware: if the consumer cancels the task + # or closes the async generator mid-delay, this raises (and no further + # request is issued) instead of blocking for the whole interval. + await anyio.sleep(self._reconnect_delay_seconds(last_retry)) + + def _decode_response( + self, + response: httpx.Response, + decoder: SSEDecoder, + text_decoder: "codecs.IncrementalDecoder", + ) -> Iterator[ServerSentEvent]: + buf = "" + for chunk in response.iter_bytes(): + buf += text_decoder.decode(chunk) + buf = self._normalize_sse_line_endings(buf) + + while "\n" in buf: + line, buf = buf.split("\n", 1) + sse = decoder.decode(line) + if sse is not None: + yield sse + + if len(buf) > MAX_LINE_SIZE: + raise SSEError( + f"SSE line exceeded maximum size of {MAX_LINE_SIZE} characters without encountering a newline" + ) + + yield from self._flush_decoder(buf, decoder, text_decoder) + + async def _adecode_response( + self, + response: httpx.Response, + decoder: SSEDecoder, + text_decoder: "codecs.IncrementalDecoder", + ) -> AsyncGenerator[ServerSentEvent, None]: + buf = "" + async for chunk in response.aiter_bytes(): + buf += text_decoder.decode(chunk) + buf = self._normalize_sse_line_endings(buf) + + while "\n" in buf: + line, buf = buf.split("\n", 1) + sse = decoder.decode(line) + if sse is not None: + yield sse + + if len(buf) > MAX_LINE_SIZE: + raise SSEError( + f"SSE line exceeded maximum size of {MAX_LINE_SIZE} characters without encountering a newline" + ) + + for sse in self._flush_decoder(buf, decoder, text_decoder): + yield sse + + def _flush_decoder( + self, + buf: str, + decoder: SSEDecoder, + text_decoder: "codecs.IncrementalDecoder", + ) -> Iterator[ServerSentEvent]: + # Flush any remaining bytes from the incremental decoder + buf += text_decoder.decode(b"", final=True) + buf = buf.replace("\r\n", "\n").replace("\r", "\n") + + if len(buf) > MAX_LINE_SIZE: + raise SSEError( + f"SSE line exceeded maximum size of {MAX_LINE_SIZE} characters without encountering a newline" + ) + + while "\n" in buf: + line, buf = buf.split("\n", 1) + sse = decoder.decode(line) + if sse is not None: + yield sse + + if buf.strip(): + sse = decoder.decode(buf) + if sse is not None: + yield sse + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + text_decoder = self._new_text_decoder() + + last_dispatched_id: Optional[str] = None + last_retry: Optional[int] = None + # Consecutive failed reconnection attempts. Reset to 0 whenever an event + # is successfully dispatched (reset-on-progress) — matching browser + # `EventSource` semantics: a server that emits >=1 event then drops on + # every connection can reconnect indefinitely. + reconnect_attempts = 0 + + # ``None`` means there is no live stream to read this iteration (e.g. a + # failed reconnect); the loop then re-evaluates the reconnect decision + # without re-reading an exhausted response. + response: Optional[httpx.Response] = self._response + # Context manager for a response we opened ourselves and must close. + # The initial response is owned by the caller, so it starts as None. + owned_cm: Optional[ContextManager[httpx.Response]] = None + try: + while True: + if response is not None: + events = self._decode_response(response, decoder, text_decoder) + while True: + try: + sse = next(events) + except StopIteration: + break + except SSEError: + # A protocol violation (e.g. an oversized line) is a + # genuine error, not a dropped connection; propagate it. + # Listed first because ``SSEError`` subclasses + # ``httpx.TransportError``. + raise + except httpx.TransportError: + # A transport error mid-stream (e.g. the server dropped + # the connection: ``ReadError``/``RemoteProtocolError``) + # is a premature end. Only swallow it when reconnection + # is configured for this stream; otherwise re-raise so a + # non-resumable stream still surfaces the error to the + # caller instead of looking like a clean completion. + # When reconnection is applicable but the attempt budget + # is exhausted, we ``break`` and end cleanly below — the + # same way an exhausted empty/error-body resume does, so + # give-up is consistent regardless of failure shape. + # ``next`` is used rather than ``for`` so this cannot + # swallow a ``GeneratorExit`` raised at a ``yield``. + if not self._reconnect_applicable(): + raise + break + yield sse + if sse.id: + last_dispatched_id = sse.id + if sse.retry is not None: + last_retry = sse.retry + reconnect_attempts = 0 + + if not self._should_reconnect(last_dispatched_id, reconnect_attempts): + return + reconnect_attempts += 1 + + self._sleep_before_reconnect(last_retry) + + # Close the previously-opened reconnect response before opening + # a new one so we never hold more than one extra connection. + if owned_cm is not None: + owned_cm.__exit__(None, None, None) + owned_cm = None + + assert self._reconnect is not None # guaranteed by _should_reconnect + try: + cm: ContextManager[httpx.Response] = self._reconnect(last_dispatched_id or "") + new_response = cm.__enter__() + except Exception: + # A failed reconnect consumes an attempt; back off and retry. + response = None + continue + owned_cm = cm + if new_response is None or not self._is_reconnect_response_usable(new_response): + # Null/empty body or a non-SSE/error response (e.g. 204/304, + # a 500, or an HTML error page): treat as a failed attempt. + response = None + continue + + response = new_response + # Drop any partial event left over from the dropped stream, but + # keep the last event id (per the SSE spec) and start a fresh + # incremental text decoder for the new connection. + decoder.reset_in_progress_event() + text_decoder = self._new_text_decoder(new_response) + finally: + if owned_cm is not None: + owned_cm.__exit__(None, None, None) + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + text_decoder = self._new_text_decoder() + + last_dispatched_id: Optional[str] = None + last_retry: Optional[int] = None + reconnect_attempts = 0 + + response: Optional[httpx.Response] = self._response + owned_cm: Optional[AsyncContextManager[httpx.Response]] = None + try: + while True: + if response is not None: + events = self._adecode_response(response, decoder, text_decoder) + while True: + try: + sse = await events.__anext__() + except StopAsyncIteration: + break + except SSEError: + # A protocol violation (e.g. an oversized line) is a + # genuine error, not a dropped connection; propagate it. + # Listed first because ``SSEError`` subclasses + # ``httpx.TransportError``. + raise + except httpx.TransportError: + # A transport error mid-stream (e.g. the server dropped + # the connection: ``ReadError``/``RemoteProtocolError``) + # is a premature end. Only swallow it when reconnection + # is configured for this stream; otherwise re-raise so a + # non-resumable stream still surfaces the error to the + # caller instead of looking like a clean completion. + # When reconnection is applicable but the attempt budget + # is exhausted, we ``break`` and end cleanly below — the + # same way an exhausted empty/error-body resume does, so + # give-up is consistent regardless of failure shape. + if not self._reconnect_applicable(): + raise + break + yield sse + if sse.id: + last_dispatched_id = sse.id + if sse.retry is not None: + last_retry = sse.retry + reconnect_attempts = 0 + + if not self._should_reconnect(last_dispatched_id, reconnect_attempts): + return + reconnect_attempts += 1 + + await self._asleep_before_reconnect(last_retry) + + if owned_cm is not None: + await owned_cm.__aexit__(None, None, None) + owned_cm = None + + assert self._reconnect is not None # guaranteed by _should_reconnect + try: + cm: AsyncContextManager[httpx.Response] = self._reconnect(last_dispatched_id or "") + new_response = await cm.__aenter__() + except Exception: + response = None + continue + owned_cm = cm + if new_response is None or not self._is_reconnect_response_usable(new_response): + response = None + continue + + response = new_response + decoder.reset_in_progress_event() + text_decoder = self._new_text_decoder(new_response) + finally: + if owned_cm is not None: + # Shield the close so a cancellation delivered while reading a + # reconnected response still fully tears the connection down + # instead of leaking it until the client is closed. + with anyio.CancelScope(shield=True): + await owned_cm.__aexit__(None, None, None) + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/src/supermemory/core/http_sse/_decoders.py b/src/supermemory/core/http_sse/_decoders.py new file mode 100644 index 00000000..1f6b35ec --- /dev/null +++ b/src/supermemory/core/http_sse/_decoders.py @@ -0,0 +1,74 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def reset_in_progress_event(self) -> None: + """Discard any partially-parsed (undispatched) event. + + Used when a stream ends mid-event before reconnecting: the buffered + ``event``/``data``/``retry`` fields of the never-dispatched event must + be dropped so they do not corrupt the first event of the reconnected + stream. Per the SSE spec the last event id is *not* reset here — it + persists across connections. + """ + self._event = "" + self._data = [] + self._retry = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/src/supermemory/core/http_sse/_exceptions.py b/src/supermemory/core/http_sse/_exceptions.py new file mode 100644 index 00000000..81605a8a --- /dev/null +++ b/src/supermemory/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/src/supermemory/core/http_sse/_models.py b/src/supermemory/core/http_sse/_models.py new file mode 100644 index 00000000..1af57f8f --- /dev/null +++ b/src/supermemory/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/src/supermemory/core/jsonable_encoder.py b/src/supermemory/core/jsonable_encoder.py new file mode 100644 index 00000000..5b0902eb --- /dev/null +++ b/src/supermemory/core/jsonable_encoder.py @@ -0,0 +1,120 @@ +# This file was auto-generated by Fern from our API Definition. + +""" +jsonable_encoder converts a Python object to a JSON-friendly dict +(e.g. datetimes to strings, Pydantic models to dicts). + +Taken from FastAPI, and made a bit simpler +https://github.com/tiangolo/fastapi/blob/master/fastapi/encoders.py +""" + +import base64 +import dataclasses +import datetime as dt +from enum import Enum +from pathlib import PurePath +from types import GeneratorType +from typing import Any, Callable, Dict, List, Optional, Set, Union + +import pydantic +from .datetime_utils import serialize_datetime +from .pydantic_utilities import ( + IS_PYDANTIC_V2, + encode_by_type, + to_jsonable_with_fallback, +) + +SetIntStr = Set[Union[int, str]] +DictIntStrAny = Dict[Union[int, str], Any] + + +def jsonable_encoder(obj: Any, custom_encoder: Optional[Dict[Any, Callable[[Any], Any]]] = None) -> Any: + custom_encoder = custom_encoder or {} + # Generated SDKs use Ellipsis (`...`) as the sentinel value for "OMIT". + # OMIT values should be excluded from serialized payloads. + if obj is Ellipsis: + return None + if custom_encoder: + if type(obj) in custom_encoder: + return custom_encoder[type(obj)](obj) + else: + for encoder_type, encoder_instance in custom_encoder.items(): + if isinstance(obj, encoder_type): + return encoder_instance(obj) + if isinstance(obj, pydantic.BaseModel): + if IS_PYDANTIC_V2: + encoder = getattr(obj.model_config, "json_encoders", {}) # type: ignore # Pydantic v2 + else: + encoder = getattr(obj.__config__, "json_encoders", {}) # type: ignore # Pydantic v1 + if custom_encoder: + encoder.update(custom_encoder) + obj_dict = obj.dict(by_alias=True) + if "__root__" in obj_dict: + obj_dict = obj_dict["__root__"] + if "root" in obj_dict: + obj_dict = obj_dict["root"] + return jsonable_encoder(obj_dict, custom_encoder=encoder) + if dataclasses.is_dataclass(obj): + obj_dict = dataclasses.asdict(obj) # type: ignore + return jsonable_encoder(obj_dict, custom_encoder=custom_encoder) + if isinstance(obj, bytes): + return base64.b64encode(obj).decode("utf-8") + if isinstance(obj, Enum): + return obj.value + if isinstance(obj, PurePath): + return str(obj) + if isinstance(obj, (str, int, float, type(None))): + return obj + if isinstance(obj, dt.datetime): + return serialize_datetime(obj) + if isinstance(obj, dt.date): + return str(obj) + if isinstance(obj, dict): + encoded_dict = {} + allowed_keys = set(obj.keys()) + for key, value in obj.items(): + if key in allowed_keys: + if value is Ellipsis: + continue + encoded_key = jsonable_encoder(key, custom_encoder=custom_encoder) + encoded_value = jsonable_encoder(value, custom_encoder=custom_encoder) + encoded_dict[encoded_key] = encoded_value + return encoded_dict + if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)): + encoded_list = [] + for item in obj: + if item is Ellipsis: + continue + encoded_list.append(jsonable_encoder(item, custom_encoder=custom_encoder)) + return encoded_list + + def fallback_serializer(o: Any) -> Any: + attempt_encode = encode_by_type(o) + if attempt_encode is not None: + return attempt_encode + + try: + data = dict(o) + except Exception as e: + errors: List[Exception] = [] + errors.append(e) + try: + data = vars(o) + except Exception as e: + errors.append(e) + raise ValueError(errors) from e + return jsonable_encoder(data, custom_encoder=custom_encoder) + + return to_jsonable_with_fallback(obj, fallback_serializer) + + +def encode_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment. + + Ensures proper string conversion for all types, including + booleans which need lowercase 'true'/'false' rather than + Python's 'True'/'False'. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return str(jsonable_encoder(obj)) diff --git a/src/supermemory/core/logging.py b/src/supermemory/core/logging.py new file mode 100644 index 00000000..e5e57245 --- /dev/null +++ b/src/supermemory/core/logging.py @@ -0,0 +1,107 @@ +# This file was auto-generated by Fern from our API Definition. + +import logging +import typing + +LogLevel = typing.Literal["debug", "info", "warn", "error"] + +_LOG_LEVEL_MAP: typing.Dict[LogLevel, int] = { + "debug": 1, + "info": 2, + "warn": 3, + "error": 4, +} + + +class ILogger(typing.Protocol): + def debug(self, message: str, **kwargs: typing.Any) -> None: ... + def info(self, message: str, **kwargs: typing.Any) -> None: ... + def warn(self, message: str, **kwargs: typing.Any) -> None: ... + def error(self, message: str, **kwargs: typing.Any) -> None: ... + + +class ConsoleLogger: + _logger: logging.Logger + + def __init__(self) -> None: + self._logger = logging.getLogger("fern") + if not self._logger.handlers: + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter("%(levelname)s - %(message)s")) + self._logger.addHandler(handler) + self._logger.setLevel(logging.DEBUG) + + def debug(self, message: str, **kwargs: typing.Any) -> None: + self._logger.debug(message, extra=kwargs) + + def info(self, message: str, **kwargs: typing.Any) -> None: + self._logger.info(message, extra=kwargs) + + def warn(self, message: str, **kwargs: typing.Any) -> None: + self._logger.warning(message, extra=kwargs) + + def error(self, message: str, **kwargs: typing.Any) -> None: + self._logger.error(message, extra=kwargs) + + +class LogConfig(typing.TypedDict, total=False): + level: LogLevel + logger: ILogger + silent: bool + + +class Logger: + _level: int + _logger: ILogger + _silent: bool + + def __init__(self, *, level: LogLevel, logger: ILogger, silent: bool) -> None: + self._level = _LOG_LEVEL_MAP[level] + self._logger = logger + self._silent = silent + + def _should_log(self, level: LogLevel) -> bool: + return not self._silent and self._level <= _LOG_LEVEL_MAP[level] + + def is_debug(self) -> bool: + return self._should_log("debug") + + def is_info(self) -> bool: + return self._should_log("info") + + def is_warn(self) -> bool: + return self._should_log("warn") + + def is_error(self) -> bool: + return self._should_log("error") + + def debug(self, message: str, **kwargs: typing.Any) -> None: + if self.is_debug(): + self._logger.debug(message, **kwargs) + + def info(self, message: str, **kwargs: typing.Any) -> None: + if self.is_info(): + self._logger.info(message, **kwargs) + + def warn(self, message: str, **kwargs: typing.Any) -> None: + if self.is_warn(): + self._logger.warn(message, **kwargs) + + def error(self, message: str, **kwargs: typing.Any) -> None: + if self.is_error(): + self._logger.error(message, **kwargs) + + +_default_logger: Logger = Logger(level="info", logger=ConsoleLogger(), silent=True) + + +def create_logger(config: typing.Optional[typing.Union[LogConfig, Logger]] = None) -> Logger: + if config is None: + return _default_logger + if isinstance(config, Logger): + return config + return Logger( + level=config.get("level", "info"), + logger=config.get("logger", ConsoleLogger()), + silent=config.get("silent", True), + ) diff --git a/src/supermemory/core/parse_error.py b/src/supermemory/core/parse_error.py new file mode 100644 index 00000000..4527c6a8 --- /dev/null +++ b/src/supermemory/core/parse_error.py @@ -0,0 +1,36 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, Optional + + +class ParsingError(Exception): + """ + Raised when the SDK fails to parse/validate a response from the server. + This typically indicates that the server returned a response whose shape + does not match the expected schema. + """ + + headers: Optional[Dict[str, str]] + status_code: Optional[int] + body: Any + cause: Optional[Exception] + + def __init__( + self, + *, + headers: Optional[Dict[str, str]] = None, + status_code: Optional[int] = None, + body: Any = None, + cause: Optional[Exception] = None, + ) -> None: + self.headers = headers + self.status_code = status_code + self.body = body + self.cause = cause + super().__init__() + if cause is not None: + self.__cause__ = cause + + def __str__(self) -> str: + cause_str = f", cause: {self.cause}" if self.cause is not None else "" + return f"headers: {self.headers}, status_code: {self.status_code}, body: {self.body}{cause_str}" diff --git a/src/supermemory/core/pydantic_utilities.py b/src/supermemory/core/pydantic_utilities.py new file mode 100644 index 00000000..6587f5e1 --- /dev/null +++ b/src/supermemory/core/pydantic_utilities.py @@ -0,0 +1,508 @@ +# This file was auto-generated by Fern from our API Definition. + +# nopycln: file +import datetime as dt +import inspect +import json +import logging +from collections import defaultdict +from dataclasses import asdict +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ClassVar, + Dict, + List, + Mapping, + Optional, + Set, + Tuple, + Type, + TypeVar, + Union, + cast, +) + +import pydantic +import typing_extensions +from pydantic.fields import FieldInfo as _FieldInfo + +_logger = logging.getLogger(__name__) + +if TYPE_CHECKING: + from .http_sse._models import ServerSentEvent + +IS_PYDANTIC_V2 = pydantic.VERSION.startswith("2.") + +if IS_PYDANTIC_V2: + _datetime_adapter = pydantic.TypeAdapter(dt.datetime) # type: ignore[attr-defined] + _date_adapter = pydantic.TypeAdapter(dt.date) # type: ignore[attr-defined] + + def parse_datetime(value: Any) -> dt.datetime: # type: ignore[misc] + if isinstance(value, dt.datetime): + return value + return _datetime_adapter.validate_python(value) + + def parse_date(value: Any) -> dt.date: # type: ignore[misc] + if isinstance(value, dt.datetime): + return value.date() + if isinstance(value, dt.date): + return value + return _date_adapter.validate_python(value) + + # Avoid importing from pydantic.v1 to maintain Python 3.14 compatibility. + from typing import get_args as get_args # type: ignore[assignment] + from typing import get_origin as get_origin # type: ignore[assignment] + + def is_literal_type(tp: Optional[Type[Any]]) -> bool: # type: ignore[misc] + return typing_extensions.get_origin(tp) is typing_extensions.Literal + + def is_union(tp: Optional[Type[Any]]) -> bool: # type: ignore[misc] + return tp is Union or typing_extensions.get_origin(tp) is Union # type: ignore[comparison-overlap] + + # Inline encoders_by_type to avoid importing from pydantic.v1.json + import re as _re + from collections import deque as _deque + from decimal import Decimal as _Decimal + from enum import Enum as _Enum + from ipaddress import ( + IPv4Address as _IPv4Address, + ) + from ipaddress import ( + IPv4Interface as _IPv4Interface, + ) + from ipaddress import ( + IPv4Network as _IPv4Network, + ) + from ipaddress import ( + IPv6Address as _IPv6Address, + ) + from ipaddress import ( + IPv6Interface as _IPv6Interface, + ) + from ipaddress import ( + IPv6Network as _IPv6Network, + ) + from pathlib import Path as _Path + from types import GeneratorType as _GeneratorType + from uuid import UUID as _UUID + + from pydantic.fields import FieldInfo as ModelField # type: ignore[no-redef, assignment] + + def _decimal_encoder(dec_value: Any) -> Any: + if dec_value.as_tuple().exponent >= 0: + return int(dec_value) + return float(dec_value) + + encoders_by_type: Dict[Type[Any], Callable[[Any], Any]] = { # type: ignore[no-redef] + bytes: lambda o: o.decode(), + dt.date: lambda o: o.isoformat(), + dt.datetime: lambda o: o.isoformat(), + dt.time: lambda o: o.isoformat(), + dt.timedelta: lambda td: td.total_seconds(), + _Decimal: _decimal_encoder, + _Enum: lambda o: o.value, + frozenset: list, + _deque: list, + _GeneratorType: list, + _IPv4Address: str, + _IPv4Interface: str, + _IPv4Network: str, + _IPv6Address: str, + _IPv6Interface: str, + _IPv6Network: str, + _Path: str, + _re.Pattern: lambda o: o.pattern, + set: list, + _UUID: str, + } +else: + from pydantic.datetime_parse import parse_date as parse_date # type: ignore[no-redef] + from pydantic.datetime_parse import parse_datetime as parse_datetime # type: ignore[no-redef] + from pydantic.fields import ModelField as ModelField # type: ignore[attr-defined, no-redef, assignment] + from pydantic.json import ENCODERS_BY_TYPE as encoders_by_type # type: ignore[no-redef] + from pydantic.typing import get_args as get_args # type: ignore[no-redef] + from pydantic.typing import get_origin as get_origin # type: ignore[no-redef] + from pydantic.typing import is_literal_type as is_literal_type # type: ignore[no-redef, assignment] + from pydantic.typing import is_union as is_union # type: ignore[no-redef] + +from .datetime_utils import serialize_datetime +from .serialization import convert_and_respect_annotation_metadata +from typing_extensions import TypeAlias + +T = TypeVar("T") +Model = TypeVar("Model", bound=pydantic.BaseModel) + + +def parse_sse_obj(sse: "ServerSentEvent", type_: Type[T]) -> T: + """ + Parse a ServerSentEvent into the appropriate type. + + This function handles data-level discrimination where the discriminator + (e.g., 'type') is inside the 'data' payload. It parses the SSE data field + as JSON and deserializes it into the target type. + + Note: Protocol-level discrimination (where the discriminator comes from + the SSE event: field) is handled at code-generation time and does not + use this function. + + Args: + sse: The ServerSentEvent object to parse + type_: The target type to deserialize into + + Returns: + The parsed object of type T + + Note: + This function is only available in SDK contexts where http_sse module exists. + """ + sse_event = asdict(sse) + data_value = sse_event.get("data") + if isinstance(data_value, str) and data_value: + try: + parsed_data = json.loads(data_value) + return parse_obj_as(type_, parsed_data) + except json.JSONDecodeError as e: + _logger.warning( + "Failed to parse SSE data field as JSON: %s, data: %s", + e, + data_value[:100] if len(data_value) > 100 else data_value, + ) + return parse_obj_as(type_, sse_event) + + +_type_adapter_cache: Dict[int, Any] = {} + + +def _get_type_adapter(type_: Type[Any]) -> Any: + key = id(type_) + adapter = _type_adapter_cache.get(key) + if adapter is None: + adapter = pydantic.TypeAdapter(type_) # type: ignore[attr-defined] + _type_adapter_cache[key] = adapter + return adapter + + +def parse_obj_as(type_: Type[T], object_: Any) -> T: + # convert_and_respect_annotation_metadata is required for TypedDict aliasing. + # + # For Pydantic models, whether we should pre-dealias depends on how the model encodes aliasing: + # - If the model uses real Pydantic aliases (pydantic.Field(alias=...)), then we must pass wire keys through + # unchanged so Pydantic can validate them. + # - If the model encodes aliasing only via FieldMetadata annotations, then we MUST pre-dealias because Pydantic + # will not recognize those aliases during validation. + if inspect.isclass(type_) and issubclass(type_, pydantic.BaseModel): + has_pydantic_aliases = False + if IS_PYDANTIC_V2: + for field_name, field_info in getattr(type_, "model_fields", {}).items(): # type: ignore[attr-defined] + alias = getattr(field_info, "alias", None) + if alias is not None and alias != field_name: + has_pydantic_aliases = True + break + else: + for field in getattr(type_, "__fields__", {}).values(): + alias = getattr(field, "alias", None) + name = getattr(field, "name", None) + if alias is not None and name is not None and alias != name: + has_pydantic_aliases = True + break + + dealiased_object = ( + object_ + if has_pydantic_aliases + else convert_and_respect_annotation_metadata(object_=object_, annotation=type_, direction="read") + ) + else: + dealiased_object = convert_and_respect_annotation_metadata(object_=object_, annotation=type_, direction="read") + if IS_PYDANTIC_V2: + adapter = _get_type_adapter(type_) + return adapter.validate_python(dealiased_object) # type: ignore[no-any-return] + return pydantic.parse_obj_as(type_, dealiased_object) + + +def to_jsonable_with_fallback(obj: Any, fallback_serializer: Callable[[Any], Any]) -> Any: + if IS_PYDANTIC_V2: + from pydantic_core import to_jsonable_python + + return to_jsonable_python(obj, fallback=fallback_serializer) + return fallback_serializer(obj) + + +class UniversalBaseModel(pydantic.BaseModel): + if IS_PYDANTIC_V2: + model_config: ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( # type: ignore[typeddict-unknown-key] + # Allow fields beginning with `model_` to be used in the model + protected_namespaces=(), + ) + + @pydantic.model_validator(mode="before") # type: ignore[attr-defined] + @classmethod + def _coerce_field_names_to_aliases(cls, data: Any) -> Any: + """ + Accept Python field names in input by rewriting them to their Pydantic aliases, + while avoiding silent collisions when a key could refer to multiple fields. + """ + if not isinstance(data, Mapping): + return data + + fields = getattr(cls, "model_fields", {}) # type: ignore[attr-defined] + name_to_alias: Dict[str, str] = {} + alias_to_name: Dict[str, str] = {} + + for name, field_info in fields.items(): + alias = getattr(field_info, "alias", None) or name + name_to_alias[name] = alias + if alias != name: + alias_to_name[alias] = name + + # Detect ambiguous keys: a key that is an alias for one field and a name for another. + ambiguous_keys = set(alias_to_name.keys()).intersection(set(name_to_alias.keys())) + for key in ambiguous_keys: + if key in data and name_to_alias[key] not in data: + raise ValueError( + f"Ambiguous input key '{key}': it is both a field name and an alias. " + "Provide the explicit alias key to disambiguate." + ) + + original_keys = set(data.keys()) + rewritten: Dict[str, Any] = dict(data) + for name, alias in name_to_alias.items(): + if alias != name and name in original_keys and alias not in rewritten: + rewritten[alias] = rewritten.pop(name) + + return rewritten + + @pydantic.model_serializer(mode="plain", when_used="json") # type: ignore[attr-defined] + def serialize_model(self) -> Any: # type: ignore[name-defined] + serialized = self.dict() # type: ignore[attr-defined] + data = {k: serialize_datetime(v) if isinstance(v, dt.datetime) else v for k, v in serialized.items()} + return data + + else: + + class Config: + smart_union = True + json_encoders = {dt.datetime: serialize_datetime} + + @pydantic.root_validator(pre=True) + def _coerce_field_names_to_aliases(cls, values: Any) -> Any: + """ + Pydantic v1 equivalent of _coerce_field_names_to_aliases. + """ + if not isinstance(values, Mapping): + return values + + fields = getattr(cls, "__fields__", {}) + name_to_alias: Dict[str, str] = {} + alias_to_name: Dict[str, str] = {} + + for name, field in fields.items(): + alias = getattr(field, "alias", None) or name + name_to_alias[name] = alias + if alias != name: + alias_to_name[alias] = name + + ambiguous_keys = set(alias_to_name.keys()).intersection(set(name_to_alias.keys())) + for key in ambiguous_keys: + if key in values and name_to_alias[key] not in values: + raise ValueError( + f"Ambiguous input key '{key}': it is both a field name and an alias. " + "Provide the explicit alias key to disambiguate." + ) + + original_keys = set(values.keys()) + rewritten: Dict[str, Any] = dict(values) + for name, alias in name_to_alias.items(): + if alias != name and name in original_keys and alias not in rewritten: + rewritten[alias] = rewritten.pop(name) + + return rewritten + + @classmethod + def model_construct(cls: Type["Model"], _fields_set: Optional[Set[str]] = None, **values: Any) -> "Model": + dealiased_object = convert_and_respect_annotation_metadata(object_=values, annotation=cls, direction="read") + return cls.construct(_fields_set, **dealiased_object) + + @classmethod + def construct(cls: Type["Model"], _fields_set: Optional[Set[str]] = None, **values: Any) -> "Model": + dealiased_object = convert_and_respect_annotation_metadata(object_=values, annotation=cls, direction="read") + if IS_PYDANTIC_V2: + return super().model_construct(_fields_set, **dealiased_object) # type: ignore[misc] + return super().construct(_fields_set, **dealiased_object) + + def json(self, **kwargs: Any) -> str: + kwargs_with_defaults = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + if IS_PYDANTIC_V2: + return super().model_dump_json(**kwargs_with_defaults) # type: ignore[misc] + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: Any) -> Dict[str, Any]: + """ + Override the default dict method to `exclude_unset` by default. This function patches + `exclude_unset` to work include fields within non-None default values. + """ + # Note: the logic here is multiplexed given the levers exposed in Pydantic V1 vs V2 + # Pydantic V1's .dict can be extremely slow, so we do not want to call it twice. + # + # We'd ideally do the same for Pydantic V2, but it shells out to a library to serialize models + # that we have less control over, and this is less intrusive than custom serializers for now. + if IS_PYDANTIC_V2: + kwargs_with_defaults_exclude_unset = { + **kwargs, + "by_alias": True, + "exclude_unset": True, + "exclude_none": False, + } + kwargs_with_defaults_exclude_none = { + **kwargs, + "by_alias": True, + "exclude_none": True, + "exclude_unset": False, + } + dict_dump = deep_union_pydantic_dicts( + super().model_dump(**kwargs_with_defaults_exclude_unset), # type: ignore[misc] + super().model_dump(**kwargs_with_defaults_exclude_none), # type: ignore[misc] + ) + + else: + _fields_set = self.__fields_set__.copy() + + fields = _get_model_fields(self.__class__) + for name, field in fields.items(): + if name not in _fields_set: + default = _get_field_default(field) + + # If the default values are non-null act like they've been set + # This effectively allows exclude_unset to work like exclude_none where + # the latter passes through intentionally set none values. + if default is not None or ("exclude_unset" in kwargs and not kwargs["exclude_unset"]): + _fields_set.add(name) + + if default is not None: + self.__fields_set__.add(name) + + kwargs_with_defaults_exclude_unset_include_fields = { + "by_alias": True, + "exclude_unset": True, + "include": _fields_set, + **kwargs, + } + + dict_dump = super().dict(**kwargs_with_defaults_exclude_unset_include_fields) + + return cast( + Dict[str, Any], + convert_and_respect_annotation_metadata(object_=dict_dump, annotation=self.__class__, direction="write"), + ) + + +def _union_list_of_pydantic_dicts(source: List[Any], destination: List[Any]) -> List[Any]: + converted_list: List[Any] = [] + for i, item in enumerate(source): + destination_value = destination[i] + if isinstance(item, dict): + converted_list.append(deep_union_pydantic_dicts(item, destination_value)) + elif isinstance(item, list): + converted_list.append(_union_list_of_pydantic_dicts(item, destination_value)) + else: + converted_list.append(item) + return converted_list + + +def deep_union_pydantic_dicts(source: Dict[str, Any], destination: Dict[str, Any]) -> Dict[str, Any]: + for key, value in source.items(): + node = destination.setdefault(key, {}) + if isinstance(value, dict): + deep_union_pydantic_dicts(value, node) + # Note: we do not do this same processing for sets given we do not have sets of models + # and given the sets are unordered, the processing of the set and matching objects would + # be non-trivial. + elif isinstance(value, list): + destination[key] = _union_list_of_pydantic_dicts(value, node) + else: + destination[key] = value + + return destination + + +if IS_PYDANTIC_V2: + + class V2RootModel(UniversalBaseModel, pydantic.RootModel): # type: ignore[misc, name-defined, type-arg] + pass + + UniversalRootModel: TypeAlias = V2RootModel # type: ignore[misc] +else: + UniversalRootModel: TypeAlias = UniversalBaseModel # type: ignore[misc, no-redef] + + +def encode_by_type(o: Any) -> Any: + encoders_by_class_tuples: Dict[Callable[[Any], Any], Tuple[Any, ...]] = defaultdict(tuple) + for type_, encoder in encoders_by_type.items(): + encoders_by_class_tuples[encoder] += (type_,) + + if type(o) in encoders_by_type: + return encoders_by_type[type(o)](o) + for encoder, classes_tuple in encoders_by_class_tuples.items(): + if isinstance(o, classes_tuple): + return encoder(o) + + +def update_forward_refs(model: Type["Model"], **localns: Any) -> None: + if IS_PYDANTIC_V2: + model.model_rebuild(raise_errors=False) # type: ignore[attr-defined] + else: + model.update_forward_refs(**localns) + + +# Mirrors Pydantic's internal typing +AnyCallable = Callable[..., Any] + + +def universal_root_validator( + pre: bool = False, +) -> Callable[[AnyCallable], AnyCallable]: + def decorator(func: AnyCallable) -> AnyCallable: + if IS_PYDANTIC_V2: + # In Pydantic v2, for RootModel we always use "before" mode + # The custom validators transform the input value before the model is created + return cast(AnyCallable, pydantic.model_validator(mode="before")(func)) # type: ignore[attr-defined] + return cast(AnyCallable, pydantic.root_validator(pre=pre)(func)) # type: ignore[call-overload] + + return decorator + + +def universal_field_validator(field_name: str, pre: bool = False) -> Callable[[AnyCallable], AnyCallable]: + def decorator(func: AnyCallable) -> AnyCallable: + if IS_PYDANTIC_V2: + return cast(AnyCallable, pydantic.field_validator(field_name, mode="before" if pre else "after")(func)) # type: ignore[attr-defined] + return cast(AnyCallable, pydantic.validator(field_name, pre=pre)(func)) + + return decorator + + +PydanticField = Union[ModelField, _FieldInfo] + + +def _get_model_fields(model: Type["Model"]) -> Mapping[str, PydanticField]: + if IS_PYDANTIC_V2: + return cast(Mapping[str, PydanticField], model.model_fields) # type: ignore[attr-defined] + return cast(Mapping[str, PydanticField], model.__fields__) + + +def _get_field_default(field: PydanticField) -> Any: + try: + value = field.get_default() # type: ignore[union-attr] + except: + value = field.default + if IS_PYDANTIC_V2: + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None + return value + return value diff --git a/src/supermemory/core/query_encoder.py b/src/supermemory/core/query_encoder.py new file mode 100644 index 00000000..3183001d --- /dev/null +++ b/src/supermemory/core/query_encoder.py @@ -0,0 +1,58 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, List, Optional, Tuple + +import pydantic + + +# Flattens dicts to be of the form {"key[subkey][subkey2]": value} where value is not a dict +def traverse_query_dict(dict_flat: Dict[str, Any], key_prefix: Optional[str] = None) -> List[Tuple[str, Any]]: + result = [] + for k, v in dict_flat.items(): + key = f"{key_prefix}[{k}]" if key_prefix is not None else k + if isinstance(v, dict): + result.extend(traverse_query_dict(v, key)) + elif isinstance(v, list): + for arr_v in v: + if isinstance(arr_v, dict): + result.extend(traverse_query_dict(arr_v, key)) + else: + result.append((key, arr_v)) + else: + result.append((key, v)) + return result + + +def single_query_encoder(query_key: str, query_value: Any) -> List[Tuple[str, Any]]: + if isinstance(query_value, pydantic.BaseModel) or isinstance(query_value, dict): + if isinstance(query_value, pydantic.BaseModel): + obj_dict = query_value.dict(by_alias=True) + else: + obj_dict = query_value + return traverse_query_dict(obj_dict, query_key) + elif isinstance(query_value, list): + encoded_values: List[Tuple[str, Any]] = [] + for value in query_value: + if isinstance(value, pydantic.BaseModel) or isinstance(value, dict): + if isinstance(value, pydantic.BaseModel): + obj_dict = value.dict(by_alias=True) + elif isinstance(value, dict): + obj_dict = value + + encoded_values.extend(single_query_encoder(query_key, obj_dict)) + else: + encoded_values.append((query_key, value)) + + return encoded_values + + return [(query_key, query_value)] + + +def encode_query(query: Optional[Dict[str, Any]]) -> Optional[List[Tuple[str, Any]]]: + if query is None: + return None + + encoded_query = [] + for k, v in query.items(): + encoded_query.extend(single_query_encoder(k, v)) + return encoded_query diff --git a/src/supermemory/core/remove_none_from_dict.py b/src/supermemory/core/remove_none_from_dict.py new file mode 100644 index 00000000..c2298143 --- /dev/null +++ b/src/supermemory/core/remove_none_from_dict.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, Mapping, Optional + + +def remove_none_from_dict(original: Mapping[str, Optional[Any]]) -> Dict[str, Any]: + new: Dict[str, Any] = {} + for key, value in original.items(): + if value is not None: + new[key] = value + return new diff --git a/src/supermemory/core/request_options.py b/src/supermemory/core/request_options.py new file mode 100644 index 00000000..caa6f669 --- /dev/null +++ b/src/supermemory/core/request_options.py @@ -0,0 +1,40 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +try: + from typing import NotRequired # type: ignore +except ImportError: + from typing_extensions import NotRequired + + +class RequestOptions(typing.TypedDict, total=False): + """ + Additional options for request-specific configuration when calling APIs via the SDK. + This is used primarily as an optional final parameter for service functions. + + Attributes: + - timeout: float. The number of seconds to await an API call before timing out. + + - timeout_in_seconds: int. Deprecated alias for `timeout`; both are in seconds. Prefer `timeout`. + + - max_retries: int. The max number of retries to attempt if the API call fails. + + - additional_headers: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's header dict + + - additional_query_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's query parameters dict + + - additional_body_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's body parameters dict + + - chunk_size: int. The size, in bytes, to process each chunk of data being streamed back within the response. This equates to leveraging `chunk_size` within `requests` or `httpx`, and is only leveraged for file downloads. + """ + + timeout: NotRequired[float] + timeout_in_seconds: NotRequired[int] + max_retries: NotRequired[int] + additional_headers: NotRequired[typing.Dict[str, typing.Any]] + additional_query_parameters: NotRequired[typing.Dict[str, typing.Any]] + additional_body_parameters: NotRequired[typing.Dict[str, typing.Any]] + chunk_size: NotRequired[int] + stream_reconnection_enabled: NotRequired[bool] + max_stream_reconnection_attempts: NotRequired[int] diff --git a/src/supermemory/core/serialization.py b/src/supermemory/core/serialization.py new file mode 100644 index 00000000..02807cc4 --- /dev/null +++ b/src/supermemory/core/serialization.py @@ -0,0 +1,383 @@ +# This file was auto-generated by Fern from our API Definition. + +import collections +import importlib +import inspect +import typing + +import pydantic +import typing_extensions + + +class FieldMetadata: + """ + Metadata class used to annotate fields to provide additional information. + + Example: + class MyDict(TypedDict): + field: typing.Annotated[str, FieldMetadata(alias="field_name")] + + Will serialize: `{"field": "value"}` + To: `{"field_name": "value"}` + """ + + alias: str + + def __init__(self, *, alias: str) -> None: + self.alias = alias + + +# Resolving type hints (typing.get_type_hints) is expensive because it eval/compiles +# forward-reference annotations. The result is constant for a given type, so we cache it. +# This is critical for hot paths like SSE event parsing, where the same (often large +# discriminated-union) type is converted on every single event. +_type_hints_cache: typing.Dict[typing.Any, typing.Dict[str, typing.Any]] = {} + + +def _get_cached_type_hints(expected_type: typing.Any) -> typing.Dict[str, typing.Any]: + try: + cached = _type_hints_cache.get(expected_type) + except TypeError: + # Unhashable type; resolve without caching. + return _resolve_type_hints(expected_type) + if cached is None: + cached = _resolve_type_hints(expected_type) + _type_hints_cache[expected_type] = cached + return cached + + +class _SdkNamespace(dict): # type: ignore[type-arg] + """Resolves names against the SDK's top-level (lazily imported) exports.""" + + def __missing__(self, name: str) -> typing.Any: + root = importlib.import_module(__name__.split(".")[0]) + try: + return getattr(root, name) + except AttributeError: + raise KeyError(name) from None + + +# Recursive types (e.g. a filter whose operands are filters) import each other only under +# TYPE_CHECKING, so their forward references can't be resolved from their own module. +_SDK_NAMESPACE = _SdkNamespace() +_forward_ref_cache: typing.Dict[str, typing.Any] = {} + + +def _resolve_forward_ref(ref: typing.ForwardRef) -> typing.Any: + name = ref.__forward_arg__ + if name not in _forward_ref_cache: + try: + _forward_ref_cache[name] = eval( + name, {"typing": typing, "typing_extensions": typing_extensions}, _SDK_NAMESPACE + ) + except NameError: + return ref + return _forward_ref_cache[name] + + +def _resolve_type_hints(expected_type: typing.Any) -> typing.Dict[str, typing.Any]: + try: + return typing_extensions.get_type_hints(expected_type, include_extras=True) + except NameError: + pass + try: + return typing_extensions.get_type_hints(expected_type, localns=_SDK_NAMESPACE, include_extras=True) + except NameError: + # The type contains a circular reference, so we use the __annotations__ attribute directly. + return getattr(expected_type, "__annotations__", {}) + + +# Whether convert_and_respect_annotation_metadata can possibly rewrite anything for a given +# annotation, i.e. whether any reachable model/TypedDict field carries a FieldMetadata alias. +# This is constant per type, so we cache it and use it to short-circuit the recursive walk. +_requires_conversion_cache: typing.Dict[typing.Any, bool] = {} + + +def _requires_conversion(type_: typing.Any) -> bool: + try: + cached = _requires_conversion_cache.get(type_) + except TypeError: + # Unhashable annotation; compute without caching. + return _compute_requires_conversion(type_, set()) + if cached is None: + cached = _compute_requires_conversion(type_, set()) + _requires_conversion_cache[type_] = cached + return cached + + +def _compute_requires_conversion(type_: typing.Any, seen: typing.Set[typing.Any]) -> bool: + clean_type = _remove_annotations(type_) + + try: + if clean_type in seen: + return False + seen = seen | {clean_type} + except TypeError: + # Unhashable type; skip cycle tracking (the type graph is finite in practice). + pass + + # Models / TypedDicts: a field alias here means we must dealias; otherwise recurse into fields. + if (inspect.isclass(clean_type) and issubclass(clean_type, pydantic.BaseModel)) or typing_extensions.is_typeddict( + clean_type + ): + annotations = _get_cached_type_hints(clean_type) + if _get_alias_to_field_name(annotations): + return True + return any(_compute_requires_conversion(hint, seen) for hint in annotations.values()) + + # Containers / unions: recurse into the type arguments (List/Set/Sequence/Dict/Union/etc.). + return any(_compute_requires_conversion(arg, seen) for arg in typing_extensions.get_args(clean_type)) + + +def convert_and_respect_annotation_metadata( + *, + object_: typing.Any, + annotation: typing.Any, + inner_type: typing.Optional[typing.Any] = None, + direction: typing.Literal["read", "write"], +) -> typing.Any: + """ + Respect the metadata annotations on a field, such as aliasing. This function effectively + manipulates the dict-form of an object to respect the metadata annotations. This is primarily used for + TypedDicts, which cannot support aliasing out of the box, and can be extended for additional + utilities, such as defaults. + + Parameters + ---------- + object_ : typing.Any + + annotation : type + The type we're looking to apply typing annotations from + + inner_type : typing.Optional[type] + + Returns + ------- + typing.Any + """ + + if object_ is None: + return None + if inner_type is None: + inner_type = annotation + # The only thing this function ever rewrites is keys that carry a FieldMetadata + # alias. If nothing in the (cached) type graph has such an alias, the conversion is + # a content-identity transform, so we can skip the entire recursive walk. This is + # the hot path for SSE streaming, where a large discriminated union would otherwise + # be traversed on every single event. + if not _requires_conversion(annotation): + return object_ + + clean_type = _remove_annotations(inner_type) + # Pydantic models + if ( + inspect.isclass(clean_type) + and issubclass(clean_type, pydantic.BaseModel) + and isinstance(object_, typing.Mapping) + ): + return _convert_mapping(object_, clean_type, direction) + # TypedDicts + if typing_extensions.is_typeddict(clean_type) and isinstance(object_, typing.Mapping): + return _convert_mapping(object_, clean_type, direction) + + if ( + typing_extensions.get_origin(clean_type) == typing.Dict + or typing_extensions.get_origin(clean_type) == dict + or clean_type == typing.Dict + ) and isinstance(object_, typing.Dict): + key_type = typing_extensions.get_args(clean_type)[0] + value_type = typing_extensions.get_args(clean_type)[1] + + return { + key: convert_and_respect_annotation_metadata( + object_=value, + annotation=annotation, + inner_type=value_type, + direction=direction, + ) + for key, value in object_.items() + } + + # If you're iterating on a string, do not bother to coerce it to a sequence. + if not isinstance(object_, str): + if ( + typing_extensions.get_origin(clean_type) == typing.Set + or typing_extensions.get_origin(clean_type) == set + or clean_type == typing.Set + ) and isinstance(object_, typing.Set): + inner_type = typing_extensions.get_args(clean_type)[0] + return { + convert_and_respect_annotation_metadata( + object_=item, + annotation=annotation, + inner_type=inner_type, + direction=direction, + ) + for item in object_ + } + elif ( + ( + typing_extensions.get_origin(clean_type) == typing.List + or typing_extensions.get_origin(clean_type) == list + or clean_type == typing.List + ) + and isinstance(object_, typing.List) + ) or ( + ( + typing_extensions.get_origin(clean_type) == typing.Sequence + or typing_extensions.get_origin(clean_type) == collections.abc.Sequence + or clean_type == typing.Sequence + ) + and isinstance(object_, typing.Sequence) + ): + inner_type = typing_extensions.get_args(clean_type)[0] + return [ + convert_and_respect_annotation_metadata( + object_=item, + annotation=annotation, + inner_type=inner_type, + direction=direction, + ) + for item in object_ + ] + + if typing_extensions.get_origin(clean_type) == typing.Union: + # We should be able to ~relatively~ safely try to convert keys against all + # member types in the union, the edge case here is if one member aliases a field + # of the same name to a different name from another member + # Or if another member aliases a field of the same name that another member does not. + for member in typing_extensions.get_args(clean_type): + object_ = convert_and_respect_annotation_metadata( + object_=object_, + annotation=annotation, + inner_type=member, + direction=direction, + ) + return object_ + + annotated_type = _get_annotation(annotation) + if annotated_type is None: + return object_ + + # If the object is not a TypedDict, a Union, or other container (list, set, sequence, etc.) + # Then we can safely call it on the recursive conversion. + return object_ + + +def _convert_mapping( + object_: typing.Mapping[str, object], + expected_type: typing.Any, + direction: typing.Literal["read", "write"], +) -> typing.Mapping[str, object]: + converted_object: typing.Dict[str, object] = {} + annotations = _get_cached_type_hints(expected_type) + aliases_to_field_names = _get_alias_to_field_name(annotations) + for key, value in object_.items(): + if direction == "read" and key in aliases_to_field_names: + dealiased_key = aliases_to_field_names.get(key) + if dealiased_key is not None: + type_ = annotations.get(dealiased_key) + else: + type_ = annotations.get(key) + # Note you can't get the annotation by the field name if you're in read mode, so you must check the aliases map + # + # So this is effectively saying if we're in write mode, and we don't have a type, or if we're in read mode and we don't have an alias + # then we can just pass the value through as is + if type_ is None: + converted_object[key] = value + elif direction == "read" and key not in aliases_to_field_names: + converted_object[key] = convert_and_respect_annotation_metadata( + object_=value, annotation=type_, direction=direction + ) + else: + converted_object[_alias_key(key, type_, direction, aliases_to_field_names)] = ( + convert_and_respect_annotation_metadata(object_=value, annotation=type_, direction=direction) + ) + return converted_object + + +def _get_annotation(type_: typing.Any) -> typing.Optional[typing.Any]: + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return None + + if maybe_annotated_type == typing_extensions.NotRequired: + type_ = typing_extensions.get_args(type_)[0] + maybe_annotated_type = typing_extensions.get_origin(type_) + + if maybe_annotated_type == typing_extensions.Annotated: + return type_ + + return None + + +def _remove_annotations(type_: typing.Any) -> typing.Any: + if isinstance(type_, typing.ForwardRef): + type_ = _resolve_forward_ref(type_) + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return type_ + + if maybe_annotated_type == typing_extensions.NotRequired: + return _remove_annotations(typing_extensions.get_args(type_)[0]) + + if maybe_annotated_type == typing_extensions.Annotated: + return _remove_annotations(typing_extensions.get_args(type_)[0]) + + return type_ + + +def get_alias_to_field_mapping(type_: typing.Any) -> typing.Dict[str, str]: + annotations = _get_cached_type_hints(type_) + return _get_alias_to_field_name(annotations) + + +def get_field_to_alias_mapping(type_: typing.Any) -> typing.Dict[str, str]: + annotations = _get_cached_type_hints(type_) + return _get_field_to_alias_name(annotations) + + +def _get_alias_to_field_name( + field_to_hint: typing.Dict[str, typing.Any], +) -> typing.Dict[str, str]: + aliases = {} + for field, hint in field_to_hint.items(): + maybe_alias = _get_alias_from_type(hint) + if maybe_alias is not None: + aliases[maybe_alias] = field + return aliases + + +def _get_field_to_alias_name( + field_to_hint: typing.Dict[str, typing.Any], +) -> typing.Dict[str, str]: + aliases = {} + for field, hint in field_to_hint.items(): + maybe_alias = _get_alias_from_type(hint) + if maybe_alias is not None: + aliases[field] = maybe_alias + return aliases + + +def _get_alias_from_type(type_: typing.Any) -> typing.Optional[str]: + maybe_annotated_type = _get_annotation(type_) + + if maybe_annotated_type is not None: + # The actual annotations are 1 onward, the first is the annotated type + annotations = typing_extensions.get_args(maybe_annotated_type)[1:] + + for annotation in annotations: + if isinstance(annotation, FieldMetadata) and annotation.alias is not None: + return annotation.alias + return None + + +def _alias_key( + key: str, + type_: typing.Any, + direction: typing.Literal["read", "write"], + aliases_to_field_names: typing.Dict[str, str], +) -> str: + if direction == "read": + return aliases_to_field_names.get(key, key) + return _get_alias_from_type(type_=type_) or key diff --git a/src/supermemory/core/unchecked_base_model.py b/src/supermemory/core/unchecked_base_model.py new file mode 100644 index 00000000..51807b4f --- /dev/null +++ b/src/supermemory/core/unchecked_base_model.py @@ -0,0 +1,539 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import enum +import inspect +import sys +import typing +import uuid + +import pydantic +import typing_extensions +from .pydantic_utilities import ( # type: ignore[attr-defined] + IS_PYDANTIC_V2, + ModelField, + UniversalBaseModel, + get_args, + get_origin, + is_literal_type, + is_union, + parse_date, + parse_datetime, + parse_obj_as, +) +from .serialization import get_field_to_alias_mapping +from pydantic_core import PydanticUndefined + + +class UnionMetadata: + discriminant: str + + def __init__(self, *, discriminant: str) -> None: + self.discriminant = discriminant + + +Model = typing.TypeVar("Model", bound=pydantic.BaseModel) + + +def _maybe_resolve_forward_ref( + type_: typing.Any, + host: typing.Optional[typing.Type[typing.Any]], +) -> typing.Any: + """Resolve a ForwardRef using the module where *host* is defined. + + Pydantic v2 + ``from __future__ import annotations`` can leave field + annotations as ``list[ForwardRef('Block')]`` even after ``model_rebuild``. + Without resolution, ``construct_type`` sees a ForwardRef (not a class) and + skips recursive model construction, leaving nested data as raw dicts. + """ + if host is None or not isinstance(type_, typing.ForwardRef): + return type_ + mod = sys.modules.get(host.__module__) + if mod is None: + return type_ + try: + return eval(type_.__forward_arg__, vars(mod)) + except Exception: + return type_ + + +class UncheckedBaseModel(UniversalBaseModel): + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + extra = pydantic.Extra.allow + + def to_dict( + self, + *, + mode: typing.Literal["json", "python"] = "python", + use_api_names: bool = True, + exclude_unset: bool = True, + exclude_defaults: bool = False, + exclude_none: bool = False, + ) -> typing.Dict[str, typing.Any]: + """Dictionary of the model, keyed by API field names unless `use_api_names=False` (3.x-compatible).""" + options: typing.Dict[str, typing.Any] = { + "by_alias": use_api_names, + "exclude_unset": exclude_unset, + "exclude_defaults": exclude_defaults, + "exclude_none": exclude_none, + } + if IS_PYDANTIC_V2: + return pydantic.BaseModel.model_dump(self, mode=mode, **options) # type: ignore[no-any-return] + if mode == "json": + import json + + return json.loads(pydantic.BaseModel.json(self, **options)) # type: ignore[no-any-return] + return pydantic.BaseModel.dict(self, **options) + + def to_json( + self, + *, + indent: typing.Optional[int] = 2, + use_api_names: bool = True, + exclude_unset: bool = True, + exclude_defaults: bool = False, + exclude_none: bool = False, + ) -> str: + """JSON string of the model, keyed by API field names unless `use_api_names=False` (3.x-compatible).""" + import json + + data = self.to_dict( + mode="json", + use_api_names=use_api_names, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + return json.dumps(data, indent=indent) + + @classmethod + def model_construct( + cls: typing.Type["Model"], + _fields_set: typing.Optional[typing.Set[str]] = None, + **values: typing.Any, + ) -> "Model": + # Fallback construct function to the specified override below. + return cls.construct(_fields_set=_fields_set, **values) + + # Allow construct to not validate model + # Implementation taken from: https://github.com/pydantic/pydantic/issues/1168#issuecomment-817742836 + @classmethod + def construct( + cls: typing.Type["Model"], + _fields_set: typing.Optional[typing.Set[str]] = None, + **values: typing.Any, + ) -> "Model": + m = cls.__new__(cls) + fields_values = {} + + if _fields_set is None: + _fields_set = set(values.keys()) + + fields = _get_model_fields(cls) + populate_by_name = _get_is_populate_by_name(cls) + field_aliases = get_field_to_alias_mapping(cls) + + for name, field in fields.items(): + # Key here is only used to pull data from the values dict + # you should always use the NAME of the field to for field_values, etc. + # because that's how the object is constructed from a pydantic perspective + key = field.alias + if (key is None or field.alias == name) and name in field_aliases: + key = field_aliases[name] + + if key is None or (key not in values and populate_by_name): # Added this to allow population by field name + key = name + + if key in values: + if IS_PYDANTIC_V2: + type_ = field.annotation # type: ignore # Pydantic v2 + else: + type_ = typing.cast(typing.Type, field.outer_type_) # type: ignore # Pydantic < v1.10.15 + + fields_values[name] = ( + construct_type(object_=values[key], type_=type_, host=cls) if type_ is not None else values[key] + ) + _fields_set.add(name) + else: + default = _get_field_default(field) + fields_values[name] = default + + # If the default values are non-null act like they've been set + # This effectively allows exclude_unset to work like exclude_none where + # the latter passes through intentionally set none values. + if default != None and default != PydanticUndefined: + _fields_set.add(name) + + # Add extras back in + extras = {} + pydantic_alias_fields = [field.alias for field in fields.values()] + internal_alias_fields = list(field_aliases.values()) + for key, value in values.items(): + # If the key is not a field by name, nor an alias to a field, then it's extra + if (key not in pydantic_alias_fields and key not in internal_alias_fields) and key not in fields: + if IS_PYDANTIC_V2: + extras[key] = value + else: + _fields_set.add(key) + fields_values[key] = value + + object.__setattr__(m, "__dict__", fields_values) + + if IS_PYDANTIC_V2: + object.__setattr__(m, "__pydantic_private__", None) + object.__setattr__(m, "__pydantic_extra__", extras) + object.__setattr__(m, "__pydantic_fields_set__", _fields_set) + else: + object.__setattr__(m, "__fields_set__", _fields_set) + m._init_private_attributes() # type: ignore # Pydantic v1 + return m + + +def _validate_collection_items_compatible(collection: typing.Any, target_type: typing.Type[typing.Any]) -> bool: + """ + Validate that all items in a collection are compatible with the target type. + + Args: + collection: The collection to validate (list, set, or dict values) + target_type: The target type to validate against + + Returns: + True if all items are compatible, False otherwise + """ + if inspect.isclass(target_type) and issubclass(target_type, pydantic.BaseModel): + for item in collection: + try: + # Try to validate the item against the target type + if isinstance(item, dict): + parse_obj_as(target_type, item) + else: + # If it's not a dict, it might already be the right type + if not isinstance(item, target_type): + return False + except Exception: + return False + return True + + +def _get_literal_field_value( + inner_type: typing.Type[typing.Any], field_name: str, field: typing.Any, object_: typing.Any +) -> typing.Any: + """Get the value of a Literal field from *object_*, checking both alias and field name.""" + name_or_alias = get_field_to_alias_mapping(inner_type).get(field_name, field_name) + pydantic_alias = getattr(field, "alias", None) + if isinstance(object_, dict): + if name_or_alias in object_: + return object_[name_or_alias] + if pydantic_alias and pydantic_alias != name_or_alias and pydantic_alias in object_: + return object_[pydantic_alias] + return None + return getattr(object_, name_or_alias, getattr(object_, pydantic_alias, None) if pydantic_alias else None) + + +def _literal_fields_match_strict(inner_type: typing.Type[typing.Any], object_: typing.Any) -> bool: + """Return True iff every Literal-typed field in *inner_type* is **present** in + *object_* and its value equals the field's declared default. + + This prevents models whose fields are all optional (e.g. ``FigureDetails``) + from vacuously matching inputs that don't carry the discriminant key at all + (e.g. ``{}`` for text blocks). For types with no Literal fields this + returns True unconditionally. + """ + fields = _get_model_fields(inner_type) + for field_name, field in fields.items(): + if IS_PYDANTIC_V2: + field_type = field.annotation # type: ignore # Pydantic v2 + else: + field_type = field.outer_type_ # type: ignore # Pydantic v1 + + if is_literal_type(field_type): # type: ignore[arg-type] + field_default = _get_field_default(field) + object_value = _get_literal_field_value(inner_type, field_name, field, object_) + if field_default != object_value: + return False + return True + + +def _convert_undiscriminated_union_type( + union_type: typing.Type[typing.Any], + object_: typing.Any, + host: typing.Optional[typing.Type[typing.Any]] = None, +) -> typing.Any: + inner_types = get_args(union_type) + if typing.Any in inner_types: + return object_ + + # When any union member carries a Literal discriminant field, require the + # discriminant key to be present AND matching before accepting a candidate. + # This prevents models with all-optional fields (e.g. FigureDetails) from + # greedily matching inputs that belong to a different variant or to a + # plain-dict fallback (e.g. EmptyBlockDetails = Dict[str, Any]). + has_literal_discriminant = any( + inspect.isclass(t) + and issubclass(t, pydantic.BaseModel) + and any( + is_literal_type( + f.annotation if IS_PYDANTIC_V2 else f.outer_type_ # type: ignore + ) + for f in _get_model_fields(t).values() + ) + for t in inner_types + ) + + for inner_type in inner_types: + # Handle lists of objects that need parsing + if get_origin(inner_type) is list and isinstance(object_, list): + list_inner_type = _maybe_resolve_forward_ref(get_args(inner_type)[0], host) + try: + if inspect.isclass(list_inner_type) and issubclass(list_inner_type, pydantic.BaseModel): + # Validate that all items in the list are compatible with the target type + if _validate_collection_items_compatible(object_, list_inner_type): + parsed_list = [parse_obj_as(object_=item, type_=list_inner_type) for item in object_] + return parsed_list + except Exception: + pass + + try: + if inspect.isclass(inner_type) and issubclass(inner_type, pydantic.BaseModel): + if has_literal_discriminant and not _literal_fields_match_strict(inner_type, object_): + continue + # Attempt a validated parse until one works + return parse_obj_as(inner_type, object_) + except Exception: + continue + + # First pass: try types where all literal fields match the object's values. + for inner_type in inner_types: + if inspect.isclass(inner_type) and issubclass(inner_type, pydantic.BaseModel): + if has_literal_discriminant: + if not _literal_fields_match_strict(inner_type, object_): + continue + else: + # Legacy lenient check: skip only when a Literal value is + # present but doesn't match (allows absent-discriminant inputs). + fields = _get_model_fields(inner_type) + literal_fields_match = True + for field_name, field in fields.items(): + if IS_PYDANTIC_V2: + field_type = field.annotation # type: ignore # Pydantic v2 + else: + field_type = field.outer_type_ # type: ignore # Pydantic v1 + + if is_literal_type(field_type): # type: ignore[arg-type] + field_default = _get_field_default(field) + object_value = _get_literal_field_value(inner_type, field_name, field, object_) + if object_value is not None and field_default != object_value: + literal_fields_match = False + break + + if not literal_fields_match: + continue + + try: + return construct_type(object_=object_, type_=inner_type, host=host) + except Exception: + continue + + # Second pass: if no literal matches, return the first successful cast. + # When a Literal discriminant is present, skip Pydantic models whose + # discriminant doesn't match so that plain-dict fallback types are reached. + for inner_type in inner_types: + try: + if has_literal_discriminant and inspect.isclass(inner_type) and issubclass(inner_type, pydantic.BaseModel): + if not _literal_fields_match_strict(inner_type, object_): + continue + return construct_type(object_=object_, type_=inner_type, host=host) + except Exception: + continue + + +def _convert_union_type( + type_: typing.Type[typing.Any], + object_: typing.Any, + host: typing.Optional[typing.Type[typing.Any]] = None, +) -> typing.Any: + base_type = get_origin(type_) or type_ + union_type = type_ + if base_type == typing_extensions.Annotated: # type: ignore[comparison-overlap] + union_type = get_args(type_)[0] + annotated_metadata = get_args(type_)[1:] + for metadata in annotated_metadata: + if isinstance(metadata, UnionMetadata): + try: + # Cast to the correct type, based on the discriminant + for inner_type in get_args(union_type): + try: + objects_discriminant = getattr(object_, metadata.discriminant) + except: + objects_discriminant = object_[metadata.discriminant] + if inner_type.__fields__[metadata.discriminant].default == objects_discriminant: + return construct_type(object_=object_, type_=inner_type, host=host) + except Exception: + # Allow to fall through to our regular union handling + pass + return _convert_undiscriminated_union_type(union_type, object_, host) + + +def construct_type( + *, + type_: typing.Type[typing.Any], + object_: typing.Any, + host: typing.Optional[typing.Type[typing.Any]] = None, +) -> typing.Any: + """ + Here we are essentially creating the same `construct` method in spirit as the above, but for all types, not just + Pydantic models. + The idea is to essentially attempt to coerce object_ to type_ (recursively) + """ + # Short circuit when dealing with optionals, don't try to coerces None to a type + if object_ is None: + return None + + base_type = get_origin(type_) or type_ + is_annotated = base_type == typing_extensions.Annotated # type: ignore[comparison-overlap] + maybe_annotation_members = get_args(type_) + is_annotated_union = is_annotated and is_union(get_origin(maybe_annotation_members[0])) + + if base_type == typing.Any: # type: ignore[comparison-overlap] + return object_ + + if base_type == dict: + if not isinstance(object_, typing.Mapping): + return object_ + + type_args = get_args(type_) + if not type_args: + return object_ + key_type, items_type = type_args + key_type = _maybe_resolve_forward_ref(key_type, host) + items_type = _maybe_resolve_forward_ref(items_type, host) + d = { + construct_type(object_=key, type_=key_type, host=host): construct_type( + object_=item, type_=items_type, host=host + ) + for key, item in object_.items() + } + return d + + if base_type == list: + if not isinstance(object_, list): + return object_ + + type_args = get_args(type_) + if not type_args: + return object_ + inner_type = _maybe_resolve_forward_ref(type_args[0], host) + return [construct_type(object_=entry, type_=inner_type, host=host) for entry in object_] + + if base_type == set: + if not isinstance(object_, set) and not isinstance(object_, list): + return object_ + + type_args = get_args(type_) + if not type_args: + return object_ + inner_type = _maybe_resolve_forward_ref(type_args[0], host) + return {construct_type(object_=entry, type_=inner_type, host=host) for entry in object_} + + if is_union(base_type) or is_annotated_union: + return _convert_union_type(type_, object_, host) + + # Cannot do an `issubclass` with a literal type, let's also just confirm we have a class before this call + if ( + object_ is not None + and not is_literal_type(type_) + and ( + (inspect.isclass(base_type) and issubclass(base_type, pydantic.BaseModel)) + or ( + is_annotated + and inspect.isclass(maybe_annotation_members[0]) + and issubclass(maybe_annotation_members[0], pydantic.BaseModel) + ) + ) + ): + if IS_PYDANTIC_V2: + return type_.model_construct(**object_) + else: + return type_.construct(**object_) + + if base_type == dt.datetime: + try: + return parse_datetime(object_) + except Exception: + return object_ + + if base_type == dt.date: + try: + return parse_date(object_) + except Exception: + return object_ + + if base_type == uuid.UUID: + try: + return uuid.UUID(object_) + except Exception: + return object_ + + if base_type == int: + try: + return int(object_) + except Exception: + return object_ + + if base_type == bool: + try: + if isinstance(object_, str): + stringified_object = object_.lower() + return stringified_object == "true" or stringified_object == "1" + + return bool(object_) + except Exception: + return object_ + + if inspect.isclass(base_type) and issubclass(base_type, enum.Enum): + try: + return base_type(object_) + except (ValueError, KeyError): + return object_ + + return object_ + + +def _get_is_populate_by_name(model: typing.Type["Model"]) -> bool: + if IS_PYDANTIC_V2: + return model.model_config.get("populate_by_name", False) # type: ignore # Pydantic v2 + return model.__config__.allow_population_by_field_name # type: ignore # Pydantic v1 + + +from pydantic.fields import FieldInfo as _FieldInfo + +PydanticField = typing.Union[ModelField, _FieldInfo] + + +# Pydantic V1 swapped the typing of __fields__'s values from ModelField to FieldInfo +# And so we try to handle both V1 cases, as well as V2 (FieldInfo from model.model_fields) +def _get_model_fields( + model: typing.Type["Model"], +) -> typing.Mapping[str, PydanticField]: + if IS_PYDANTIC_V2: + return model.model_fields # type: ignore # Pydantic v2 + else: + return model.__fields__ # type: ignore # Pydantic v1 + + +def _get_field_default(field: PydanticField) -> typing.Any: + try: + value = field.get_default() # type: ignore # Pydantic < v1.10.15 + except: + value = field.default + if IS_PYDANTIC_V2: + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None + return value + return value diff --git a/src/supermemory/documents/__init__.py b/src/supermemory/documents/__init__.py new file mode 100644 index 00000000..5cde0202 --- /dev/null +++ b/src/supermemory/documents/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/supermemory/documents/client.py b/src/supermemory/documents/client.py new file mode 100644 index 00000000..7b9a1532 --- /dev/null +++ b/src/supermemory/documents/client.py @@ -0,0 +1,1138 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx + +from .. import core +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..requests.document_input import DocumentInputParams +from ..requests.metadata_value import MetadataValueParams +from ..types.batch_add_result import BatchAddResult +from ..types.delete_documents_result import DeleteDocumentsResult +from ..types.document import Document +from ..types.document_attachment import DocumentAttachment +from ..types.document_ref import DocumentRef +from ..types.dreaming_mode import DreamingMode +from ..types.file_type import FileType +from ..types.file_upload_result import FileUploadResult +from ..types.task_type import TaskType +from .raw_client import AsyncRawDocumentsClient, RawDocumentsClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class DocumentsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawDocumentsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawDocumentsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawDocumentsClient + """ + return self._raw_client + + def delete( + self, + namespace: str, + *, + ids: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> DeleteDocumentsResult: + """ + Permanently remove documents and their derived knowledge by document ID or caller-defined ID. Each requested ID is handled independently so successful deletions are preserved when another ID fails. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + ids : typing.Sequence[str] + Document identifiers to permanently delete from this namespace + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DeleteDocumentsResult + Document deletion completed + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.documents.delete( + namespace="user_alex", + ids=["my-doc-123"], + ) + """ + _response = self._raw_client.delete(namespace, ids=ids, request_options=request_options) + return _response.data + + def batch_add( + self, + namespace: str, + *, + documents: typing.Sequence[DocumentInputParams], + task_type: typing.Optional[TaskType] = None, + dreaming: typing.Optional[DreamingMode] = None, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> BatchAddResult: + """ + Build a knowledge base efficiently by ingesting up to 600 text or URL documents at once. Existing caller-defined IDs append new information using the same semantics as single-document ingestion. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + documents : typing.Sequence[DocumentInputParams] + Documents to ingest or append in one request + + task_type : typing.Optional[TaskType] + Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + + dreaming : typing.Optional[DreamingMode] + Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + BatchAddResult + Batch accepted for processing + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.documents.batch_add( + namespace="user_alex", + documents=[ + { + "content": "Supermemory turns unstructured content into evolving memory." + } + ], + ) + """ + _response = self._raw_client.batch_add( + namespace, documents=documents, task_type=task_type, dreaming=dreaming, request_options=request_options + ) + return _response.data + + def get( + self, + namespace: str, + id: str, + *, + attach: typing.Optional[typing.Union[DocumentAttachment, typing.Sequence[DocumentAttachment]]] = None, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> Document: + """ + Retrieve a document's canonical content, metadata, and processing state by document ID or caller-defined ID. Optionally attach its source chunks, derived memories, or both in the same response. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + id : str + The public document ID + + attach : typing.Optional[typing.Union[DocumentAttachment, typing.Sequence[DocumentAttachment]]] + Child resources to include. Repeat the parameter to attach chunks, memories, or both. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Document + Document retrieved successfully + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.documents.get( + namespace="user_alex", + id="my-doc-123", + ) + """ + _response = self._raw_client.get(namespace, id, attach=attach, request_options=request_options) + return _response.data + + def update( + self, + namespace: str, + id: str, + *, + task_type: typing.Optional[TaskType] = None, + dreaming: typing.Optional[DreamingMode] = None, + content: typing.Optional[str] = OMIT, + supporting_context: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, MetadataValueParams]] = OMIT, + group: typing.Optional[typing.Dict[str, MetadataValueParams]] = OMIT, + date: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> DocumentRef: + """ + Refresh an existing document without changing its stable ID. Supplied content replaces the canonical content and is reprocessed; omitted fields remain unchanged. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + id : str + The public document ID + + task_type : typing.Optional[TaskType] + Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + + dreaming : typing.Optional[DreamingMode] + Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + + content : typing.Optional[str] + The content to process. This may be plaintext or a URL to supported rich content. + + supporting_context : typing.Optional[str] + Context used to guide memory extraction within this namespace. Max 1500 characters. + + metadata : typing.Optional[typing.Dict[str, MetadataValueParams]] + Arbitrary metadata attached to the document and derived data + + group : typing.Optional[typing.Dict[str, MetadataValueParams]] + Metadata values that isolate related context within the namespace + + date : typing.Optional[str] + When the source content is from, in ISO 8601 format + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DocumentRef + Document updated successfully + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.documents.update( + namespace="user_alex", + id="my-doc-123", + ) + """ + _response = self._raw_client.update( + namespace, + id, + task_type=task_type, + dreaming=dreaming, + content=content, + supporting_context=supporting_context, + metadata=metadata, + group=group, + date=date, + request_options=request_options, + ) + return _response.data + + def upload_file( + self, + namespace: str, + *, + file: core.File, + task_type: typing.Optional[TaskType] = None, + dreaming: typing.Optional[DreamingMode] = None, + file_type: typing.Optional[FileType] = None, + mime_type: typing.Optional[str] = None, + supporting_context: typing.Optional[str] = OMIT, + metadata: typing.Optional[str] = OMIT, + group: typing.Optional[str] = OMIT, + date: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> FileUploadResult: + """ + Transform an uploaded file into searchable knowledge and learned memory. The response returns as soon as ingestion is safely queued while extraction and memory formation continue asynchronously. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + file : core.File + See core.File for more documentation + + task_type : typing.Optional[TaskType] + Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + + dreaming : typing.Optional[DreamingMode] + Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + + file_type : typing.Optional[FileType] + Explicit source type used when automatic inference is insufficient + + mime_type : typing.Optional[str] + Explicit MIME type used when upload metadata is insufficient + + supporting_context : typing.Optional[str] + Context used to guide memory extraction within this namespace. Max 1500 characters. + + metadata : typing.Optional[str] + JSON-encoded metadata to attach to the document and its derived memories + + group : typing.Optional[str] + JSON-encoded metadata used to keep related context isolated during processing and recall + + date : typing.Optional[str] + When the source content is from, in ISO 8601 format + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + FileUploadResult + File accepted for processing + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.documents.upload_file( + namespace="user_alex", + ) + """ + _response = self._raw_client.upload_file( + namespace, + file=file, + task_type=task_type, + dreaming=dreaming, + file_type=file_type, + mime_type=mime_type, + supporting_context=supporting_context, + metadata=metadata, + group=group, + date=date, + request_options=request_options, + ) + return _response.data + + def replace_with_file( + self, + namespace: str, + id: str, + *, + file: core.File, + task_type: typing.Optional[TaskType] = None, + dreaming: typing.Optional[DreamingMode] = None, + file_type: typing.Optional[FileType] = None, + mime_type: typing.Optional[str] = None, + supporting_context: typing.Optional[str] = OMIT, + metadata: typing.Optional[str] = OMIT, + group: typing.Optional[str] = OMIT, + date: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> FileUploadResult: + """ + Replace an existing document with a new file while keeping its stable document ID. Content and caller metadata are overwritten, then the document is reprocessed asynchronously. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + id : str + The public document ID + + file : core.File + See core.File for more documentation + + task_type : typing.Optional[TaskType] + Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + + dreaming : typing.Optional[DreamingMode] + Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + + file_type : typing.Optional[FileType] + Explicit source type used when automatic inference is insufficient + + mime_type : typing.Optional[str] + Explicit MIME type used when upload metadata is insufficient + + supporting_context : typing.Optional[str] + Context used to guide memory extraction within this namespace. Max 1500 characters. + + metadata : typing.Optional[str] + JSON-encoded metadata to attach to the document and its derived memories + + group : typing.Optional[str] + JSON-encoded metadata used to keep related context isolated during processing and recall + + date : typing.Optional[str] + When the source content is from, in ISO 8601 format + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + FileUploadResult + Replacement accepted for processing + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.documents.replace_with_file( + namespace="user_alex", + id="my-doc-123", + ) + """ + _response = self._raw_client.replace_with_file( + namespace, + id, + file=file, + task_type=task_type, + dreaming=dreaming, + file_type=file_type, + mime_type=mime_type, + supporting_context=supporting_context, + metadata=metadata, + group=group, + date=date, + request_options=request_options, + ) + return _response.data + + def update_file( + self, + namespace: str, + id: str, + *, + task_type: typing.Optional[TaskType] = None, + dreaming: typing.Optional[DreamingMode] = None, + file_type: typing.Optional[FileType] = None, + mime_type: typing.Optional[str] = None, + file: typing.Optional[core.File] = OMIT, + supporting_context: typing.Optional[str] = OMIT, + metadata: typing.Optional[str] = OMIT, + group: typing.Optional[str] = OMIT, + date: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> FileUploadResult: + """ + Refresh only the file-backed fields you provide. Supplying a file replaces the canonical content; omitted metadata and processing context remain unchanged. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + id : str + The public document ID + + task_type : typing.Optional[TaskType] + Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + + dreaming : typing.Optional[DreamingMode] + Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + + file_type : typing.Optional[FileType] + Explicit source type used when automatic inference is insufficient + + mime_type : typing.Optional[str] + Explicit MIME type used when upload metadata is insufficient + + file : typing.Optional[core.File] + See core.File for more documentation + + supporting_context : typing.Optional[str] + Context used to guide memory extraction within this namespace. Max 1500 characters. + + metadata : typing.Optional[str] + JSON-encoded metadata to attach to the document and its derived memories + + group : typing.Optional[str] + JSON-encoded metadata used to keep related context isolated during processing and recall + + date : typing.Optional[str] + When the source content is from, in ISO 8601 format + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + FileUploadResult + Update accepted for processing + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.documents.update_file( + namespace="user_alex", + id="my-doc-123", + ) + """ + _response = self._raw_client.update_file( + namespace, + id, + task_type=task_type, + dreaming=dreaming, + file_type=file_type, + mime_type=mime_type, + file=file, + supporting_context=supporting_context, + metadata=metadata, + group=group, + date=date, + request_options=request_options, + ) + return _response.data + + +class AsyncDocumentsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawDocumentsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawDocumentsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawDocumentsClient + """ + return self._raw_client + + async def delete( + self, + namespace: str, + *, + ids: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> DeleteDocumentsResult: + """ + Permanently remove documents and their derived knowledge by document ID or caller-defined ID. Each requested ID is handled independently so successful deletions are preserved when another ID fails. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + ids : typing.Sequence[str] + Document identifiers to permanently delete from this namespace + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DeleteDocumentsResult + Document deletion completed + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.documents.delete( + namespace="user_alex", + ids=["my-doc-123"], + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete(namespace, ids=ids, request_options=request_options) + return _response.data + + async def batch_add( + self, + namespace: str, + *, + documents: typing.Sequence[DocumentInputParams], + task_type: typing.Optional[TaskType] = None, + dreaming: typing.Optional[DreamingMode] = None, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> BatchAddResult: + """ + Build a knowledge base efficiently by ingesting up to 600 text or URL documents at once. Existing caller-defined IDs append new information using the same semantics as single-document ingestion. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + documents : typing.Sequence[DocumentInputParams] + Documents to ingest or append in one request + + task_type : typing.Optional[TaskType] + Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + + dreaming : typing.Optional[DreamingMode] + Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + BatchAddResult + Batch accepted for processing + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.documents.batch_add( + namespace="user_alex", + documents=[ + { + "content": "Supermemory turns unstructured content into evolving memory." + } + ], + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.batch_add( + namespace, documents=documents, task_type=task_type, dreaming=dreaming, request_options=request_options + ) + return _response.data + + async def get( + self, + namespace: str, + id: str, + *, + attach: typing.Optional[typing.Union[DocumentAttachment, typing.Sequence[DocumentAttachment]]] = None, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> Document: + """ + Retrieve a document's canonical content, metadata, and processing state by document ID or caller-defined ID. Optionally attach its source chunks, derived memories, or both in the same response. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + id : str + The public document ID + + attach : typing.Optional[typing.Union[DocumentAttachment, typing.Sequence[DocumentAttachment]]] + Child resources to include. Repeat the parameter to attach chunks, memories, or both. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Document + Document retrieved successfully + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.documents.get( + namespace="user_alex", + id="my-doc-123", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get(namespace, id, attach=attach, request_options=request_options) + return _response.data + + async def update( + self, + namespace: str, + id: str, + *, + task_type: typing.Optional[TaskType] = None, + dreaming: typing.Optional[DreamingMode] = None, + content: typing.Optional[str] = OMIT, + supporting_context: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, MetadataValueParams]] = OMIT, + group: typing.Optional[typing.Dict[str, MetadataValueParams]] = OMIT, + date: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> DocumentRef: + """ + Refresh an existing document without changing its stable ID. Supplied content replaces the canonical content and is reprocessed; omitted fields remain unchanged. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + id : str + The public document ID + + task_type : typing.Optional[TaskType] + Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + + dreaming : typing.Optional[DreamingMode] + Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + + content : typing.Optional[str] + The content to process. This may be plaintext or a URL to supported rich content. + + supporting_context : typing.Optional[str] + Context used to guide memory extraction within this namespace. Max 1500 characters. + + metadata : typing.Optional[typing.Dict[str, MetadataValueParams]] + Arbitrary metadata attached to the document and derived data + + group : typing.Optional[typing.Dict[str, MetadataValueParams]] + Metadata values that isolate related context within the namespace + + date : typing.Optional[str] + When the source content is from, in ISO 8601 format + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DocumentRef + Document updated successfully + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.documents.update( + namespace="user_alex", + id="my-doc-123", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.update( + namespace, + id, + task_type=task_type, + dreaming=dreaming, + content=content, + supporting_context=supporting_context, + metadata=metadata, + group=group, + date=date, + request_options=request_options, + ) + return _response.data + + async def upload_file( + self, + namespace: str, + *, + file: core.File, + task_type: typing.Optional[TaskType] = None, + dreaming: typing.Optional[DreamingMode] = None, + file_type: typing.Optional[FileType] = None, + mime_type: typing.Optional[str] = None, + supporting_context: typing.Optional[str] = OMIT, + metadata: typing.Optional[str] = OMIT, + group: typing.Optional[str] = OMIT, + date: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> FileUploadResult: + """ + Transform an uploaded file into searchable knowledge and learned memory. The response returns as soon as ingestion is safely queued while extraction and memory formation continue asynchronously. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + file : core.File + See core.File for more documentation + + task_type : typing.Optional[TaskType] + Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + + dreaming : typing.Optional[DreamingMode] + Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + + file_type : typing.Optional[FileType] + Explicit source type used when automatic inference is insufficient + + mime_type : typing.Optional[str] + Explicit MIME type used when upload metadata is insufficient + + supporting_context : typing.Optional[str] + Context used to guide memory extraction within this namespace. Max 1500 characters. + + metadata : typing.Optional[str] + JSON-encoded metadata to attach to the document and its derived memories + + group : typing.Optional[str] + JSON-encoded metadata used to keep related context isolated during processing and recall + + date : typing.Optional[str] + When the source content is from, in ISO 8601 format + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + FileUploadResult + File accepted for processing + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.documents.upload_file( + namespace="user_alex", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.upload_file( + namespace, + file=file, + task_type=task_type, + dreaming=dreaming, + file_type=file_type, + mime_type=mime_type, + supporting_context=supporting_context, + metadata=metadata, + group=group, + date=date, + request_options=request_options, + ) + return _response.data + + async def replace_with_file( + self, + namespace: str, + id: str, + *, + file: core.File, + task_type: typing.Optional[TaskType] = None, + dreaming: typing.Optional[DreamingMode] = None, + file_type: typing.Optional[FileType] = None, + mime_type: typing.Optional[str] = None, + supporting_context: typing.Optional[str] = OMIT, + metadata: typing.Optional[str] = OMIT, + group: typing.Optional[str] = OMIT, + date: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> FileUploadResult: + """ + Replace an existing document with a new file while keeping its stable document ID. Content and caller metadata are overwritten, then the document is reprocessed asynchronously. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + id : str + The public document ID + + file : core.File + See core.File for more documentation + + task_type : typing.Optional[TaskType] + Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + + dreaming : typing.Optional[DreamingMode] + Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + + file_type : typing.Optional[FileType] + Explicit source type used when automatic inference is insufficient + + mime_type : typing.Optional[str] + Explicit MIME type used when upload metadata is insufficient + + supporting_context : typing.Optional[str] + Context used to guide memory extraction within this namespace. Max 1500 characters. + + metadata : typing.Optional[str] + JSON-encoded metadata to attach to the document and its derived memories + + group : typing.Optional[str] + JSON-encoded metadata used to keep related context isolated during processing and recall + + date : typing.Optional[str] + When the source content is from, in ISO 8601 format + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + FileUploadResult + Replacement accepted for processing + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.documents.replace_with_file( + namespace="user_alex", + id="my-doc-123", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.replace_with_file( + namespace, + id, + file=file, + task_type=task_type, + dreaming=dreaming, + file_type=file_type, + mime_type=mime_type, + supporting_context=supporting_context, + metadata=metadata, + group=group, + date=date, + request_options=request_options, + ) + return _response.data + + async def update_file( + self, + namespace: str, + id: str, + *, + task_type: typing.Optional[TaskType] = None, + dreaming: typing.Optional[DreamingMode] = None, + file_type: typing.Optional[FileType] = None, + mime_type: typing.Optional[str] = None, + file: typing.Optional[core.File] = OMIT, + supporting_context: typing.Optional[str] = OMIT, + metadata: typing.Optional[str] = OMIT, + group: typing.Optional[str] = OMIT, + date: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> FileUploadResult: + """ + Refresh only the file-backed fields you provide. Supplying a file replaces the canonical content; omitted metadata and processing context remain unchanged. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + id : str + The public document ID + + task_type : typing.Optional[TaskType] + Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + + dreaming : typing.Optional[DreamingMode] + Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + + file_type : typing.Optional[FileType] + Explicit source type used when automatic inference is insufficient + + mime_type : typing.Optional[str] + Explicit MIME type used when upload metadata is insufficient + + file : typing.Optional[core.File] + See core.File for more documentation + + supporting_context : typing.Optional[str] + Context used to guide memory extraction within this namespace. Max 1500 characters. + + metadata : typing.Optional[str] + JSON-encoded metadata to attach to the document and its derived memories + + group : typing.Optional[str] + JSON-encoded metadata used to keep related context isolated during processing and recall + + date : typing.Optional[str] + When the source content is from, in ISO 8601 format + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + FileUploadResult + Update accepted for processing + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.documents.update_file( + namespace="user_alex", + id="my-doc-123", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.update_file( + namespace, + id, + task_type=task_type, + dreaming=dreaming, + file_type=file_type, + mime_type=mime_type, + file=file, + supporting_context=supporting_context, + metadata=metadata, + group=group, + date=date, + request_options=request_options, + ) + return _response.data diff --git a/src/supermemory/documents/raw_client.py b/src/supermemory/documents/raw_client.py new file mode 100644 index 00000000..e81caed4 --- /dev/null +++ b/src/supermemory/documents/raw_client.py @@ -0,0 +1,2182 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx +from json.decoder import JSONDecodeError + +from .. import core +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import encode_path_param +from ..core.parse_error import ParsingError +from ..core.request_options import RequestOptions +from ..core.serialization import convert_and_respect_annotation_metadata +from ..core.unchecked_base_model import construct_type +from ..errors.bad_request_error import BadRequestError +from ..errors.conflict_error import ConflictError +from ..errors.forbidden_error import ForbiddenError +from ..errors.internal_server_error import InternalServerError +from ..errors.not_found_error import NotFoundError +from ..errors.payment_required_error import PaymentRequiredError +from ..errors.unauthorized_error import UnauthorizedError +from ..requests.document_input import DocumentInputParams +from ..requests.metadata_value import MetadataValueParams +from ..types.bad_request_error_body import BadRequestErrorBody +from ..types.batch_add_result import BatchAddResult +from ..types.delete_documents_result import DeleteDocumentsResult +from ..types.document import Document +from ..types.document_attachment import DocumentAttachment +from ..types.document_ref import DocumentRef +from ..types.dreaming_mode import DreamingMode +from ..types.error_response import ErrorResponse +from ..types.file_type import FileType +from ..types.file_upload_result import FileUploadResult +from ..types.task_type import TaskType +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawDocumentsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def delete( + self, + namespace: str, + *, + ids: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[DeleteDocumentsResult]: + """ + Permanently remove documents and their derived knowledge by document ID or caller-defined ID. Each requested ID is handled independently so successful deletions are preserved when another ID fails. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + ids : typing.Sequence[str] + Document identifiers to permanently delete from this namespace + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[DeleteDocumentsResult] + Document deletion completed + """ + _response = self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/document", + method="DELETE", + json={ + "ids": ids, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DeleteDocumentsResult, + construct_type( + type_=DeleteDocumentsResult, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def batch_add( + self, + namespace: str, + *, + documents: typing.Sequence[DocumentInputParams], + task_type: typing.Optional[TaskType] = None, + dreaming: typing.Optional[DreamingMode] = None, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[BatchAddResult]: + """ + Build a knowledge base efficiently by ingesting up to 600 text or URL documents at once. Existing caller-defined IDs append new information using the same semantics as single-document ingestion. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + documents : typing.Sequence[DocumentInputParams] + Documents to ingest or append in one request + + task_type : typing.Optional[TaskType] + Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + + dreaming : typing.Optional[DreamingMode] + Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[BatchAddResult] + Batch accepted for processing + """ + _response = self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/document/batch", + method="POST", + params={ + "taskType": task_type, + "dreaming": dreaming, + }, + json={ + "documents": convert_and_respect_annotation_metadata( + object_=documents, annotation=typing.Sequence[DocumentInputParams], direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + BatchAddResult, + construct_type( + type_=BatchAddResult, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get( + self, + namespace: str, + id: str, + *, + attach: typing.Optional[typing.Union[DocumentAttachment, typing.Sequence[DocumentAttachment]]] = None, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[Document]: + """ + Retrieve a document's canonical content, metadata, and processing state by document ID or caller-defined ID. Optionally attach its source chunks, derived memories, or both in the same response. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + id : str + The public document ID + + attach : typing.Optional[typing.Union[DocumentAttachment, typing.Sequence[DocumentAttachment]]] + Child resources to include. Repeat the parameter to attach chunks, memories, or both. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[Document] + Document retrieved successfully + """ + _response = self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/document/{encode_path_param(id)}", + method="GET", + params={ + "attach": attach, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Document, + construct_type( + type_=Document, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def update( + self, + namespace: str, + id: str, + *, + task_type: typing.Optional[TaskType] = None, + dreaming: typing.Optional[DreamingMode] = None, + content: typing.Optional[str] = OMIT, + supporting_context: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, MetadataValueParams]] = OMIT, + group: typing.Optional[typing.Dict[str, MetadataValueParams]] = OMIT, + date: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[DocumentRef]: + """ + Refresh an existing document without changing its stable ID. Supplied content replaces the canonical content and is reprocessed; omitted fields remain unchanged. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + id : str + The public document ID + + task_type : typing.Optional[TaskType] + Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + + dreaming : typing.Optional[DreamingMode] + Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + + content : typing.Optional[str] + The content to process. This may be plaintext or a URL to supported rich content. + + supporting_context : typing.Optional[str] + Context used to guide memory extraction within this namespace. Max 1500 characters. + + metadata : typing.Optional[typing.Dict[str, MetadataValueParams]] + Arbitrary metadata attached to the document and derived data + + group : typing.Optional[typing.Dict[str, MetadataValueParams]] + Metadata values that isolate related context within the namespace + + date : typing.Optional[str] + When the source content is from, in ISO 8601 format + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[DocumentRef] + Document updated successfully + """ + _response = self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/document/{encode_path_param(id)}", + method="PATCH", + params={ + "taskType": task_type, + "dreaming": dreaming, + }, + json={ + "content": content, + "supportingContext": supporting_context, + "metadata": convert_and_respect_annotation_metadata( + object_=metadata, annotation=typing.Dict[str, MetadataValueParams], direction="write" + ), + "group": convert_and_respect_annotation_metadata( + object_=group, annotation=typing.Dict[str, MetadataValueParams], direction="write" + ), + "date": date, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DocumentRef, + construct_type( + type_=DocumentRef, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def upload_file( + self, + namespace: str, + *, + file: core.File, + task_type: typing.Optional[TaskType] = None, + dreaming: typing.Optional[DreamingMode] = None, + file_type: typing.Optional[FileType] = None, + mime_type: typing.Optional[str] = None, + supporting_context: typing.Optional[str] = OMIT, + metadata: typing.Optional[str] = OMIT, + group: typing.Optional[str] = OMIT, + date: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[FileUploadResult]: + """ + Transform an uploaded file into searchable knowledge and learned memory. The response returns as soon as ingestion is safely queued while extraction and memory formation continue asynchronously. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + file : core.File + See core.File for more documentation + + task_type : typing.Optional[TaskType] + Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + + dreaming : typing.Optional[DreamingMode] + Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + + file_type : typing.Optional[FileType] + Explicit source type used when automatic inference is insufficient + + mime_type : typing.Optional[str] + Explicit MIME type used when upload metadata is insufficient + + supporting_context : typing.Optional[str] + Context used to guide memory extraction within this namespace. Max 1500 characters. + + metadata : typing.Optional[str] + JSON-encoded metadata to attach to the document and its derived memories + + group : typing.Optional[str] + JSON-encoded metadata used to keep related context isolated during processing and recall + + date : typing.Optional[str] + When the source content is from, in ISO 8601 format + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[FileUploadResult] + File accepted for processing + """ + _response = self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/document/file", + method="POST", + params={ + "taskType": task_type, + "dreaming": dreaming, + "fileType": file_type, + "mimeType": mime_type, + }, + data={ + "supportingContext": supporting_context, + "metadata": metadata, + "group": group, + "date": date, + }, + files={ + "file": file, + }, + request_options=request_options, + omit=OMIT, + force_multipart=True, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + FileUploadResult, + construct_type( + type_=FileUploadResult, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def replace_with_file( + self, + namespace: str, + id: str, + *, + file: core.File, + task_type: typing.Optional[TaskType] = None, + dreaming: typing.Optional[DreamingMode] = None, + file_type: typing.Optional[FileType] = None, + mime_type: typing.Optional[str] = None, + supporting_context: typing.Optional[str] = OMIT, + metadata: typing.Optional[str] = OMIT, + group: typing.Optional[str] = OMIT, + date: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[FileUploadResult]: + """ + Replace an existing document with a new file while keeping its stable document ID. Content and caller metadata are overwritten, then the document is reprocessed asynchronously. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + id : str + The public document ID + + file : core.File + See core.File for more documentation + + task_type : typing.Optional[TaskType] + Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + + dreaming : typing.Optional[DreamingMode] + Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + + file_type : typing.Optional[FileType] + Explicit source type used when automatic inference is insufficient + + mime_type : typing.Optional[str] + Explicit MIME type used when upload metadata is insufficient + + supporting_context : typing.Optional[str] + Context used to guide memory extraction within this namespace. Max 1500 characters. + + metadata : typing.Optional[str] + JSON-encoded metadata to attach to the document and its derived memories + + group : typing.Optional[str] + JSON-encoded metadata used to keep related context isolated during processing and recall + + date : typing.Optional[str] + When the source content is from, in ISO 8601 format + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[FileUploadResult] + Replacement accepted for processing + """ + _response = self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/document/file/{encode_path_param(id)}", + method="POST", + params={ + "taskType": task_type, + "dreaming": dreaming, + "fileType": file_type, + "mimeType": mime_type, + }, + data={ + "supportingContext": supporting_context, + "metadata": metadata, + "group": group, + "date": date, + }, + files={ + "file": file, + }, + request_options=request_options, + omit=OMIT, + force_multipart=True, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + FileUploadResult, + construct_type( + type_=FileUploadResult, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def update_file( + self, + namespace: str, + id: str, + *, + task_type: typing.Optional[TaskType] = None, + dreaming: typing.Optional[DreamingMode] = None, + file_type: typing.Optional[FileType] = None, + mime_type: typing.Optional[str] = None, + file: typing.Optional[core.File] = OMIT, + supporting_context: typing.Optional[str] = OMIT, + metadata: typing.Optional[str] = OMIT, + group: typing.Optional[str] = OMIT, + date: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[FileUploadResult]: + """ + Refresh only the file-backed fields you provide. Supplying a file replaces the canonical content; omitted metadata and processing context remain unchanged. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + id : str + The public document ID + + task_type : typing.Optional[TaskType] + Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + + dreaming : typing.Optional[DreamingMode] + Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + + file_type : typing.Optional[FileType] + Explicit source type used when automatic inference is insufficient + + mime_type : typing.Optional[str] + Explicit MIME type used when upload metadata is insufficient + + file : typing.Optional[core.File] + See core.File for more documentation + + supporting_context : typing.Optional[str] + Context used to guide memory extraction within this namespace. Max 1500 characters. + + metadata : typing.Optional[str] + JSON-encoded metadata to attach to the document and its derived memories + + group : typing.Optional[str] + JSON-encoded metadata used to keep related context isolated during processing and recall + + date : typing.Optional[str] + When the source content is from, in ISO 8601 format + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[FileUploadResult] + Update accepted for processing + """ + _response = self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/document/file/{encode_path_param(id)}", + method="PATCH", + params={ + "taskType": task_type, + "dreaming": dreaming, + "fileType": file_type, + "mimeType": mime_type, + }, + data={ + "supportingContext": supporting_context, + "metadata": metadata, + "group": group, + "date": date, + }, + files={ + **({"file": file} if file is not None else {}), + }, + request_options=request_options, + omit=OMIT, + force_multipart=True, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + FileUploadResult, + construct_type( + type_=FileUploadResult, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawDocumentsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def delete( + self, + namespace: str, + *, + ids: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[DeleteDocumentsResult]: + """ + Permanently remove documents and their derived knowledge by document ID or caller-defined ID. Each requested ID is handled independently so successful deletions are preserved when another ID fails. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + ids : typing.Sequence[str] + Document identifiers to permanently delete from this namespace + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[DeleteDocumentsResult] + Document deletion completed + """ + _response = await self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/document", + method="DELETE", + json={ + "ids": ids, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DeleteDocumentsResult, + construct_type( + type_=DeleteDocumentsResult, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def batch_add( + self, + namespace: str, + *, + documents: typing.Sequence[DocumentInputParams], + task_type: typing.Optional[TaskType] = None, + dreaming: typing.Optional[DreamingMode] = None, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[BatchAddResult]: + """ + Build a knowledge base efficiently by ingesting up to 600 text or URL documents at once. Existing caller-defined IDs append new information using the same semantics as single-document ingestion. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + documents : typing.Sequence[DocumentInputParams] + Documents to ingest or append in one request + + task_type : typing.Optional[TaskType] + Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + + dreaming : typing.Optional[DreamingMode] + Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[BatchAddResult] + Batch accepted for processing + """ + _response = await self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/document/batch", + method="POST", + params={ + "taskType": task_type, + "dreaming": dreaming, + }, + json={ + "documents": convert_and_respect_annotation_metadata( + object_=documents, annotation=typing.Sequence[DocumentInputParams], direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + BatchAddResult, + construct_type( + type_=BatchAddResult, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get( + self, + namespace: str, + id: str, + *, + attach: typing.Optional[typing.Union[DocumentAttachment, typing.Sequence[DocumentAttachment]]] = None, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[Document]: + """ + Retrieve a document's canonical content, metadata, and processing state by document ID or caller-defined ID. Optionally attach its source chunks, derived memories, or both in the same response. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + id : str + The public document ID + + attach : typing.Optional[typing.Union[DocumentAttachment, typing.Sequence[DocumentAttachment]]] + Child resources to include. Repeat the parameter to attach chunks, memories, or both. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[Document] + Document retrieved successfully + """ + _response = await self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/document/{encode_path_param(id)}", + method="GET", + params={ + "attach": attach, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Document, + construct_type( + type_=Document, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def update( + self, + namespace: str, + id: str, + *, + task_type: typing.Optional[TaskType] = None, + dreaming: typing.Optional[DreamingMode] = None, + content: typing.Optional[str] = OMIT, + supporting_context: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, MetadataValueParams]] = OMIT, + group: typing.Optional[typing.Dict[str, MetadataValueParams]] = OMIT, + date: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[DocumentRef]: + """ + Refresh an existing document without changing its stable ID. Supplied content replaces the canonical content and is reprocessed; omitted fields remain unchanged. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + id : str + The public document ID + + task_type : typing.Optional[TaskType] + Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + + dreaming : typing.Optional[DreamingMode] + Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + + content : typing.Optional[str] + The content to process. This may be plaintext or a URL to supported rich content. + + supporting_context : typing.Optional[str] + Context used to guide memory extraction within this namespace. Max 1500 characters. + + metadata : typing.Optional[typing.Dict[str, MetadataValueParams]] + Arbitrary metadata attached to the document and derived data + + group : typing.Optional[typing.Dict[str, MetadataValueParams]] + Metadata values that isolate related context within the namespace + + date : typing.Optional[str] + When the source content is from, in ISO 8601 format + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[DocumentRef] + Document updated successfully + """ + _response = await self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/document/{encode_path_param(id)}", + method="PATCH", + params={ + "taskType": task_type, + "dreaming": dreaming, + }, + json={ + "content": content, + "supportingContext": supporting_context, + "metadata": convert_and_respect_annotation_metadata( + object_=metadata, annotation=typing.Dict[str, MetadataValueParams], direction="write" + ), + "group": convert_and_respect_annotation_metadata( + object_=group, annotation=typing.Dict[str, MetadataValueParams], direction="write" + ), + "date": date, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DocumentRef, + construct_type( + type_=DocumentRef, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def upload_file( + self, + namespace: str, + *, + file: core.File, + task_type: typing.Optional[TaskType] = None, + dreaming: typing.Optional[DreamingMode] = None, + file_type: typing.Optional[FileType] = None, + mime_type: typing.Optional[str] = None, + supporting_context: typing.Optional[str] = OMIT, + metadata: typing.Optional[str] = OMIT, + group: typing.Optional[str] = OMIT, + date: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[FileUploadResult]: + """ + Transform an uploaded file into searchable knowledge and learned memory. The response returns as soon as ingestion is safely queued while extraction and memory formation continue asynchronously. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + file : core.File + See core.File for more documentation + + task_type : typing.Optional[TaskType] + Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + + dreaming : typing.Optional[DreamingMode] + Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + + file_type : typing.Optional[FileType] + Explicit source type used when automatic inference is insufficient + + mime_type : typing.Optional[str] + Explicit MIME type used when upload metadata is insufficient + + supporting_context : typing.Optional[str] + Context used to guide memory extraction within this namespace. Max 1500 characters. + + metadata : typing.Optional[str] + JSON-encoded metadata to attach to the document and its derived memories + + group : typing.Optional[str] + JSON-encoded metadata used to keep related context isolated during processing and recall + + date : typing.Optional[str] + When the source content is from, in ISO 8601 format + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[FileUploadResult] + File accepted for processing + """ + _response = await self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/document/file", + method="POST", + params={ + "taskType": task_type, + "dreaming": dreaming, + "fileType": file_type, + "mimeType": mime_type, + }, + data={ + "supportingContext": supporting_context, + "metadata": metadata, + "group": group, + "date": date, + }, + files={ + "file": file, + }, + request_options=request_options, + omit=OMIT, + force_multipart=True, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + FileUploadResult, + construct_type( + type_=FileUploadResult, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def replace_with_file( + self, + namespace: str, + id: str, + *, + file: core.File, + task_type: typing.Optional[TaskType] = None, + dreaming: typing.Optional[DreamingMode] = None, + file_type: typing.Optional[FileType] = None, + mime_type: typing.Optional[str] = None, + supporting_context: typing.Optional[str] = OMIT, + metadata: typing.Optional[str] = OMIT, + group: typing.Optional[str] = OMIT, + date: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[FileUploadResult]: + """ + Replace an existing document with a new file while keeping its stable document ID. Content and caller metadata are overwritten, then the document is reprocessed asynchronously. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + id : str + The public document ID + + file : core.File + See core.File for more documentation + + task_type : typing.Optional[TaskType] + Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + + dreaming : typing.Optional[DreamingMode] + Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + + file_type : typing.Optional[FileType] + Explicit source type used when automatic inference is insufficient + + mime_type : typing.Optional[str] + Explicit MIME type used when upload metadata is insufficient + + supporting_context : typing.Optional[str] + Context used to guide memory extraction within this namespace. Max 1500 characters. + + metadata : typing.Optional[str] + JSON-encoded metadata to attach to the document and its derived memories + + group : typing.Optional[str] + JSON-encoded metadata used to keep related context isolated during processing and recall + + date : typing.Optional[str] + When the source content is from, in ISO 8601 format + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[FileUploadResult] + Replacement accepted for processing + """ + _response = await self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/document/file/{encode_path_param(id)}", + method="POST", + params={ + "taskType": task_type, + "dreaming": dreaming, + "fileType": file_type, + "mimeType": mime_type, + }, + data={ + "supportingContext": supporting_context, + "metadata": metadata, + "group": group, + "date": date, + }, + files={ + "file": file, + }, + request_options=request_options, + omit=OMIT, + force_multipart=True, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + FileUploadResult, + construct_type( + type_=FileUploadResult, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def update_file( + self, + namespace: str, + id: str, + *, + task_type: typing.Optional[TaskType] = None, + dreaming: typing.Optional[DreamingMode] = None, + file_type: typing.Optional[FileType] = None, + mime_type: typing.Optional[str] = None, + file: typing.Optional[core.File] = OMIT, + supporting_context: typing.Optional[str] = OMIT, + metadata: typing.Optional[str] = OMIT, + group: typing.Optional[str] = OMIT, + date: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[FileUploadResult]: + """ + Refresh only the file-backed fields you provide. Supplying a file replaces the canonical content; omitted metadata and processing context remain unchanged. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + id : str + The public document ID + + task_type : typing.Optional[TaskType] + Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + + dreaming : typing.Optional[DreamingMode] + Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + + file_type : typing.Optional[FileType] + Explicit source type used when automatic inference is insufficient + + mime_type : typing.Optional[str] + Explicit MIME type used when upload metadata is insufficient + + file : typing.Optional[core.File] + See core.File for more documentation + + supporting_context : typing.Optional[str] + Context used to guide memory extraction within this namespace. Max 1500 characters. + + metadata : typing.Optional[str] + JSON-encoded metadata to attach to the document and its derived memories + + group : typing.Optional[str] + JSON-encoded metadata used to keep related context isolated during processing and recall + + date : typing.Optional[str] + When the source content is from, in ISO 8601 format + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[FileUploadResult] + Update accepted for processing + """ + _response = await self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/document/file/{encode_path_param(id)}", + method="PATCH", + params={ + "taskType": task_type, + "dreaming": dreaming, + "fileType": file_type, + "mimeType": mime_type, + }, + data={ + "supportingContext": supporting_context, + "metadata": metadata, + "group": group, + "date": date, + }, + files={ + **({"file": file} if file is not None else {}), + }, + request_options=request_options, + omit=OMIT, + force_multipart=True, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + FileUploadResult, + construct_type( + type_=FileUploadResult, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/supermemory/environment.py b/src/supermemory/environment.py new file mode 100644 index 00000000..fcb002db --- /dev/null +++ b/src/supermemory/environment.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import enum + + +class SupermemoryEnvironment(enum.Enum): + DEFAULT = "https://api.supermemory.ai" diff --git a/src/supermemory/errors/__init__.py b/src/supermemory/errors/__init__.py new file mode 100644 index 00000000..0cf3b941 --- /dev/null +++ b/src/supermemory/errors/__init__.py @@ -0,0 +1,59 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .bad_request_error import BadRequestError + from .conflict_error import ConflictError + from .forbidden_error import ForbiddenError + from .internal_server_error import InternalServerError + from .not_found_error import NotFoundError + from .payment_required_error import PaymentRequiredError + from .service_unavailable_error import ServiceUnavailableError + from .unauthorized_error import UnauthorizedError +_dynamic_imports: typing.Dict[str, str] = { + "BadRequestError": ".bad_request_error", + "ConflictError": ".conflict_error", + "ForbiddenError": ".forbidden_error", + "InternalServerError": ".internal_server_error", + "NotFoundError": ".not_found_error", + "PaymentRequiredError": ".payment_required_error", + "ServiceUnavailableError": ".service_unavailable_error", + "UnauthorizedError": ".unauthorized_error", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "BadRequestError", + "ConflictError", + "ForbiddenError", + "InternalServerError", + "NotFoundError", + "PaymentRequiredError", + "ServiceUnavailableError", + "UnauthorizedError", +] diff --git a/src/supermemory/errors/bad_request_error.py b/src/supermemory/errors/bad_request_error.py new file mode 100644 index 00000000..a4541df3 --- /dev/null +++ b/src/supermemory/errors/bad_request_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.bad_request_error_body import BadRequestErrorBody + + +class BadRequestError(ApiError): + def __init__(self, body: BadRequestErrorBody, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=400, headers=headers, body=body) diff --git a/src/supermemory/errors/conflict_error.py b/src/supermemory/errors/conflict_error.py new file mode 100644 index 00000000..a53ae507 --- /dev/null +++ b/src/supermemory/errors/conflict_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.error_response import ErrorResponse + + +class ConflictError(ApiError): + def __init__(self, body: ErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=409, headers=headers, body=body) diff --git a/src/supermemory/errors/forbidden_error.py b/src/supermemory/errors/forbidden_error.py new file mode 100644 index 00000000..0841085e --- /dev/null +++ b/src/supermemory/errors/forbidden_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.error_response import ErrorResponse + + +class ForbiddenError(ApiError): + def __init__(self, body: ErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=403, headers=headers, body=body) diff --git a/src/supermemory/errors/internal_server_error.py b/src/supermemory/errors/internal_server_error.py new file mode 100644 index 00000000..d7a796c6 --- /dev/null +++ b/src/supermemory/errors/internal_server_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.error_response import ErrorResponse + + +class InternalServerError(ApiError): + def __init__(self, body: ErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=500, headers=headers, body=body) diff --git a/src/supermemory/errors/not_found_error.py b/src/supermemory/errors/not_found_error.py new file mode 100644 index 00000000..3b2a2d1f --- /dev/null +++ b/src/supermemory/errors/not_found_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.error_response import ErrorResponse + + +class NotFoundError(ApiError): + def __init__(self, body: ErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=404, headers=headers, body=body) diff --git a/src/supermemory/errors/payment_required_error.py b/src/supermemory/errors/payment_required_error.py new file mode 100644 index 00000000..f03f31c5 --- /dev/null +++ b/src/supermemory/errors/payment_required_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.error_response import ErrorResponse + + +class PaymentRequiredError(ApiError): + def __init__(self, body: ErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=402, headers=headers, body=body) diff --git a/src/supermemory/errors/service_unavailable_error.py b/src/supermemory/errors/service_unavailable_error.py new file mode 100644 index 00000000..3758d2ea --- /dev/null +++ b/src/supermemory/errors/service_unavailable_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.error_response import ErrorResponse + + +class ServiceUnavailableError(ApiError): + def __init__(self, body: ErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=503, headers=headers, body=body) diff --git a/src/supermemory/errors/unauthorized_error.py b/src/supermemory/errors/unauthorized_error.py new file mode 100644 index 00000000..00a614a3 --- /dev/null +++ b/src/supermemory/errors/unauthorized_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError +from ..types.error_response import ErrorResponse + + +class UnauthorizedError(ApiError): + def __init__(self, body: ErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=401, headers=headers, body=body) diff --git a/src/supermemory/lib/.keep b/src/supermemory/lib/.keep deleted file mode 100644 index 5e2c99fd..00000000 --- a/src/supermemory/lib/.keep +++ /dev/null @@ -1,4 +0,0 @@ -File generated from our OpenAPI spec by Stainless. - -This directory can be used to store custom files to expand the SDK. -It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/src/supermemory/memories/__init__.py b/src/supermemory/memories/__init__.py new file mode 100644 index 00000000..5cde0202 --- /dev/null +++ b/src/supermemory/memories/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/supermemory/memories/client.py b/src/supermemory/memories/client.py new file mode 100644 index 00000000..6b4c406f --- /dev/null +++ b/src/supermemory/memories/client.py @@ -0,0 +1,255 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.forget_result import ForgetResult +from .raw_client import AsyncRawMemoriesClient, RawMemoriesClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class MemoriesClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawMemoriesClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawMemoriesClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawMemoriesClient + """ + return self._raw_client + + def forget( + self, + namespace: str, + *, + ids: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> ForgetResult: + """ + Remove exact memories from normal recall while preserving their audit history. Each ID is handled independently and any missing or ineligible memory is reported without rolling back successful changes. + + Parameters + ---------- + namespace : str + Namespace containing the memories to forget. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + ids : typing.Sequence[str] + Memory identifiers to remove from normal recall + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ForgetResult + Exact memory forget completed + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.memories.forget( + namespace="user_alex", + ids=["mem_abc123"], + ) + """ + _response = self._raw_client.forget(namespace, ids=ids, request_options=request_options) + return _response.data + + def forget_matching( + self, + namespace: str, + *, + query: str, + dry_run: bool, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> ForgetResult: + """ + Describe what should be forgotten in natural language, then preview or apply the matching set. Use dry-run results with the exact-ID endpoint when you need a reviewable, drift-free workflow. + + Parameters + ---------- + namespace : str + Namespace containing the memories to forget. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + query : str + Natural-language description of the memories that should be forgotten + + dry_run : bool + When true, preview matching memories without changing their recall state + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ForgetResult + Semantic memory forget completed or previewed + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.memories.forget_matching( + namespace="user_alex", + query="everything about the old pricing plans", + dry_run=True, + ) + """ + _response = self._raw_client.forget_matching( + namespace, query=query, dry_run=dry_run, request_options=request_options + ) + return _response.data + + +class AsyncMemoriesClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawMemoriesClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawMemoriesClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawMemoriesClient + """ + return self._raw_client + + async def forget( + self, + namespace: str, + *, + ids: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> ForgetResult: + """ + Remove exact memories from normal recall while preserving their audit history. Each ID is handled independently and any missing or ineligible memory is reported without rolling back successful changes. + + Parameters + ---------- + namespace : str + Namespace containing the memories to forget. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + ids : typing.Sequence[str] + Memory identifiers to remove from normal recall + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ForgetResult + Exact memory forget completed + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.memories.forget( + namespace="user_alex", + ids=["mem_abc123"], + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.forget(namespace, ids=ids, request_options=request_options) + return _response.data + + async def forget_matching( + self, + namespace: str, + *, + query: str, + dry_run: bool, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> ForgetResult: + """ + Describe what should be forgotten in natural language, then preview or apply the matching set. Use dry-run results with the exact-ID endpoint when you need a reviewable, drift-free workflow. + + Parameters + ---------- + namespace : str + Namespace containing the memories to forget. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + query : str + Natural-language description of the memories that should be forgotten + + dry_run : bool + When true, preview matching memories without changing their recall state + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ForgetResult + Semantic memory forget completed or previewed + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.memories.forget_matching( + namespace="user_alex", + query="everything about the old pricing plans", + dry_run=True, + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.forget_matching( + namespace, query=query, dry_run=dry_run, request_options=request_options + ) + return _response.data diff --git a/src/supermemory/memories/raw_client.py b/src/supermemory/memories/raw_client.py new file mode 100644 index 00000000..b86352bf --- /dev/null +++ b/src/supermemory/memories/raw_client.py @@ -0,0 +1,465 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import encode_path_param +from ..core.parse_error import ParsingError +from ..core.request_options import RequestOptions +from ..core.unchecked_base_model import construct_type +from ..errors.bad_request_error import BadRequestError +from ..errors.forbidden_error import ForbiddenError +from ..errors.internal_server_error import InternalServerError +from ..errors.unauthorized_error import UnauthorizedError +from ..types.bad_request_error_body import BadRequestErrorBody +from ..types.error_response import ErrorResponse +from ..types.forget_result import ForgetResult +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawMemoriesClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def forget( + self, + namespace: str, + *, + ids: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[ForgetResult]: + """ + Remove exact memories from normal recall while preserving their audit history. Each ID is handled independently and any missing or ineligible memory is reported without rolling back successful changes. + + Parameters + ---------- + namespace : str + Namespace containing the memories to forget. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + ids : typing.Sequence[str] + Memory identifiers to remove from normal recall + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ForgetResult] + Exact memory forget completed + """ + _response = self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/memories", + method="DELETE", + json={ + "ids": ids, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ForgetResult, + construct_type( + type_=ForgetResult, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def forget_matching( + self, + namespace: str, + *, + query: str, + dry_run: bool, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[ForgetResult]: + """ + Describe what should be forgotten in natural language, then preview or apply the matching set. Use dry-run results with the exact-ID endpoint when you need a reviewable, drift-free workflow. + + Parameters + ---------- + namespace : str + Namespace containing the memories to forget. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + query : str + Natural-language description of the memories that should be forgotten + + dry_run : bool + When true, preview matching memories without changing their recall state + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ForgetResult] + Semantic memory forget completed or previewed + """ + _response = self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/memories/semantic", + method="DELETE", + json={ + "query": query, + "dryRun": dry_run, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ForgetResult, + construct_type( + type_=ForgetResult, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawMemoriesClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def forget( + self, + namespace: str, + *, + ids: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[ForgetResult]: + """ + Remove exact memories from normal recall while preserving their audit history. Each ID is handled independently and any missing or ineligible memory is reported without rolling back successful changes. + + Parameters + ---------- + namespace : str + Namespace containing the memories to forget. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + ids : typing.Sequence[str] + Memory identifiers to remove from normal recall + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ForgetResult] + Exact memory forget completed + """ + _response = await self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/memories", + method="DELETE", + json={ + "ids": ids, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ForgetResult, + construct_type( + type_=ForgetResult, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def forget_matching( + self, + namespace: str, + *, + query: str, + dry_run: bool, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[ForgetResult]: + """ + Describe what should be forgotten in natural language, then preview or apply the matching set. Use dry-run results with the exact-ID endpoint when you need a reviewable, drift-free workflow. + + Parameters + ---------- + namespace : str + Namespace containing the memories to forget. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + query : str + Natural-language description of the memories that should be forgotten + + dry_run : bool + When true, preview matching memories without changing their recall state + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ForgetResult] + Semantic memory forget completed or previewed + """ + _response = await self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/memories/semantic", + method="DELETE", + json={ + "query": query, + "dryRun": dry_run, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ForgetResult, + construct_type( + type_=ForgetResult, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/supermemory/namespaces/__init__.py b/src/supermemory/namespaces/__init__.py new file mode 100644 index 00000000..5cde0202 --- /dev/null +++ b/src/supermemory/namespaces/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/supermemory/namespaces/client.py b/src/supermemory/namespaces/client.py new file mode 100644 index 00000000..d1dc0b3e --- /dev/null +++ b/src/supermemory/namespaces/client.py @@ -0,0 +1,407 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.namespace import Namespace +from ..types.namespace_deleted import NamespaceDeleted +from ..types.namespace_details import NamespaceDetails +from .raw_client import AsyncRawNamespacesClient, RawNamespacesClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class NamespacesClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawNamespacesClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawNamespacesClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawNamespacesClient + """ + return self._raw_client + + def list( + self, + *, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> typing.List[Namespace]: + """ + Discover every namespace available to the caller, including its purpose, document volume, memory count, and lifecycle timestamps. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + typing.List[Namespace] + Namespaces available to the caller + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.namespaces.list() + """ + _response = self._raw_client.list(request_options=request_options) + return _response.data + + def get( + self, + namespace: str, + *, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> NamespaceDetails: + """ + Read the supporting context that helps Supermemory understand content and form better memories inside this namespace. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + NamespaceDetails + Namespace settings + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.namespaces.get( + namespace="user_alex", + ) + """ + _response = self._raw_client.get(namespace, request_options=request_options) + return _response.data + + def delete( + self, + namespace: str, + *, + move_to: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> NamespaceDeleted: + """ + Retire a namespace by permanently deleting its content, or preserve that knowledge by moving everything into another namespace first. Moves are queued and complete asynchronously. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + move_to : typing.Optional[str] + Destination namespace that should receive this namespace's content before deletion. Omit to permanently delete the content. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + NamespaceDeleted + Namespace permanently deleted + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.namespaces.delete( + namespace="user_alex", + ) + """ + _response = self._raw_client.delete(namespace, move_to=move_to, request_options=request_options) + return _response.data + + def update( + self, + namespace: str, + *, + supporting_context: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> NamespaceDetails: + """ + Shape how Supermemory understands an existing namespace by updating the background context used during ingestion and memory formation. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + supporting_context : typing.Optional[str] + Background that guides how Supermemory interprets documents and forms memories. Max 1500 characters. Set null to clear it. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + NamespaceDetails + Updated namespace settings + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.namespaces.update( + namespace="user_alex", + ) + """ + _response = self._raw_client.update( + namespace, supporting_context=supporting_context, request_options=request_options + ) + return _response.data + + +class AsyncNamespacesClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawNamespacesClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawNamespacesClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawNamespacesClient + """ + return self._raw_client + + async def list( + self, + *, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> typing.List[Namespace]: + """ + Discover every namespace available to the caller, including its purpose, document volume, memory count, and lifecycle timestamps. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + typing.List[Namespace] + Namespaces available to the caller + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.namespaces.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list(request_options=request_options) + return _response.data + + async def get( + self, + namespace: str, + *, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> NamespaceDetails: + """ + Read the supporting context that helps Supermemory understand content and form better memories inside this namespace. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + NamespaceDetails + Namespace settings + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.namespaces.get( + namespace="user_alex", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get(namespace, request_options=request_options) + return _response.data + + async def delete( + self, + namespace: str, + *, + move_to: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> NamespaceDeleted: + """ + Retire a namespace by permanently deleting its content, or preserve that knowledge by moving everything into another namespace first. Moves are queued and complete asynchronously. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + move_to : typing.Optional[str] + Destination namespace that should receive this namespace's content before deletion. Omit to permanently delete the content. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + NamespaceDeleted + Namespace permanently deleted + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.namespaces.delete( + namespace="user_alex", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete(namespace, move_to=move_to, request_options=request_options) + return _response.data + + async def update( + self, + namespace: str, + *, + supporting_context: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> NamespaceDetails: + """ + Shape how Supermemory understands an existing namespace by updating the background context used during ingestion and memory formation. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + supporting_context : typing.Optional[str] + Background that guides how Supermemory interprets documents and forms memories. Max 1500 characters. Set null to clear it. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + NamespaceDetails + Updated namespace settings + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.namespaces.update( + namespace="user_alex", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.update( + namespace, supporting_context=supporting_context, request_options=request_options + ) + return _response.data diff --git a/src/supermemory/namespaces/raw_client.py b/src/supermemory/namespaces/raw_client.py new file mode 100644 index 00000000..051d1c99 --- /dev/null +++ b/src/supermemory/namespaces/raw_client.py @@ -0,0 +1,893 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import encode_path_param +from ..core.parse_error import ParsingError +from ..core.request_options import RequestOptions +from ..core.unchecked_base_model import construct_type +from ..errors.bad_request_error import BadRequestError +from ..errors.conflict_error import ConflictError +from ..errors.forbidden_error import ForbiddenError +from ..errors.internal_server_error import InternalServerError +from ..errors.not_found_error import NotFoundError +from ..errors.unauthorized_error import UnauthorizedError +from ..types.bad_request_error_body import BadRequestErrorBody +from ..types.error_response import ErrorResponse +from ..types.namespace import Namespace +from ..types.namespace_deleted import NamespaceDeleted +from ..types.namespace_details import NamespaceDetails +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawNamespacesClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list( + self, + *, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[typing.List[Namespace]]: + """ + Discover every namespace available to the caller, including its purpose, document volume, memory count, and lifecycle timestamps. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[typing.List[Namespace]] + Namespaces available to the caller + """ + _response = self._client_wrapper.httpx_client.request( + "ns", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + typing.List[Namespace], + construct_type( + type_=typing.List[Namespace], # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get( + self, + namespace: str, + *, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[NamespaceDetails]: + """ + Read the supporting context that helps Supermemory understand content and form better memories inside this namespace. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[NamespaceDetails] + Namespace settings + """ + _response = self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + NamespaceDetails, + construct_type( + type_=NamespaceDetails, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def delete( + self, + namespace: str, + *, + move_to: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[NamespaceDeleted]: + """ + Retire a namespace by permanently deleting its content, or preserve that knowledge by moving everything into another namespace first. Moves are queued and complete asynchronously. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + move_to : typing.Optional[str] + Destination namespace that should receive this namespace's content before deletion. Omit to permanently delete the content. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[NamespaceDeleted] + Namespace permanently deleted + """ + _response = self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}", + method="DELETE", + json={ + "moveTo": move_to, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + NamespaceDeleted, + construct_type( + type_=NamespaceDeleted, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def update( + self, + namespace: str, + *, + supporting_context: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[NamespaceDetails]: + """ + Shape how Supermemory understands an existing namespace by updating the background context used during ingestion and memory formation. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + supporting_context : typing.Optional[str] + Background that guides how Supermemory interprets documents and forms memories. Max 1500 characters. Set null to clear it. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[NamespaceDetails] + Updated namespace settings + """ + _response = self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}", + method="PATCH", + json={ + "supportingContext": supporting_context, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + NamespaceDetails, + construct_type( + type_=NamespaceDetails, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawNamespacesClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( + self, + *, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[typing.List[Namespace]]: + """ + Discover every namespace available to the caller, including its purpose, document volume, memory count, and lifecycle timestamps. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[typing.List[Namespace]] + Namespaces available to the caller + """ + _response = await self._client_wrapper.httpx_client.request( + "ns", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + typing.List[Namespace], + construct_type( + type_=typing.List[Namespace], # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get( + self, + namespace: str, + *, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[NamespaceDetails]: + """ + Read the supporting context that helps Supermemory understand content and form better memories inside this namespace. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[NamespaceDetails] + Namespace settings + """ + _response = await self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + NamespaceDetails, + construct_type( + type_=NamespaceDetails, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def delete( + self, + namespace: str, + *, + move_to: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[NamespaceDeleted]: + """ + Retire a namespace by permanently deleting its content, or preserve that knowledge by moving everything into another namespace first. Moves are queued and complete asynchronously. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + move_to : typing.Optional[str] + Destination namespace that should receive this namespace's content before deletion. Omit to permanently delete the content. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[NamespaceDeleted] + Namespace permanently deleted + """ + _response = await self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}", + method="DELETE", + json={ + "moveTo": move_to, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + NamespaceDeleted, + construct_type( + type_=NamespaceDeleted, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def update( + self, + namespace: str, + *, + supporting_context: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[NamespaceDetails]: + """ + Shape how Supermemory understands an existing namespace by updating the background context used during ingestion and memory formation. + + Parameters + ---------- + namespace : str + Namespace identifier. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents and memories. + + supporting_context : typing.Optional[str] + Background that guides how Supermemory interprets documents and forms memories. Max 1500 characters. Set null to clear it. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[NamespaceDetails] + Updated namespace settings + """ + _response = await self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}", + method="PATCH", + json={ + "supportingContext": supporting_context, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + NamespaceDetails, + construct_type( + type_=NamespaceDetails, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/supermemory/organization/__init__.py b/src/supermemory/organization/__init__.py new file mode 100644 index 00000000..5cde0202 --- /dev/null +++ b/src/supermemory/organization/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/supermemory/organization/client.py b/src/supermemory/organization/client.py new file mode 100644 index 00000000..b45665c2 --- /dev/null +++ b/src/supermemory/organization/client.py @@ -0,0 +1,209 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.organization import Organization +from .raw_client import AsyncRawOrganizationClient, RawOrganizationClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class OrganizationClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawOrganizationClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawOrganizationClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawOrganizationClient + """ + return self._raw_client + + def get( + self, + *, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> Organization: + """ + Read the shared background that guides memory formation across the organization, together with the number of active namespaces. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Organization + Organization settings retrieved successfully + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.organization.get() + """ + _response = self._raw_client.get(request_options=request_options) + return _response.data + + def update( + self, + *, + organizational_context: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> Organization: + """ + Give Supermemory organization-wide background that improves how content is interpreted across every namespace. Send null to remove the existing context. + + Parameters + ---------- + organizational_context : typing.Optional[str] + Shared background that guides memory formation across every namespace. Set null to clear it. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Organization + Organization settings updated successfully + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.organization.update() + """ + _response = self._raw_client.update( + organizational_context=organizational_context, request_options=request_options + ) + return _response.data + + +class AsyncOrganizationClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawOrganizationClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawOrganizationClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawOrganizationClient + """ + return self._raw_client + + async def get( + self, + *, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> Organization: + """ + Read the shared background that guides memory formation across the organization, together with the number of active namespaces. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Organization + Organization settings retrieved successfully + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.organization.get() + + + asyncio.run(main()) + """ + _response = await self._raw_client.get(request_options=request_options) + return _response.data + + async def update( + self, + *, + organizational_context: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> Organization: + """ + Give Supermemory organization-wide background that improves how content is interpreted across every namespace. Send null to remove the existing context. + + Parameters + ---------- + organizational_context : typing.Optional[str] + Shared background that guides memory formation across every namespace. Set null to clear it. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Organization + Organization settings updated successfully + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.organization.update() + + + asyncio.run(main()) + """ + _response = await self._raw_client.update( + organizational_context=organizational_context, request_options=request_options + ) + return _response.data diff --git a/src/supermemory/organization/raw_client.py b/src/supermemory/organization/raw_client.py new file mode 100644 index 00000000..aa64efef --- /dev/null +++ b/src/supermemory/organization/raw_client.py @@ -0,0 +1,394 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.parse_error import ParsingError +from ..core.request_options import RequestOptions +from ..core.unchecked_base_model import construct_type +from ..errors.bad_request_error import BadRequestError +from ..errors.forbidden_error import ForbiddenError +from ..errors.internal_server_error import InternalServerError +from ..errors.unauthorized_error import UnauthorizedError +from ..types.bad_request_error_body import BadRequestErrorBody +from ..types.error_response import ErrorResponse +from ..types.organization import Organization +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawOrganizationClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get( + self, + *, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[Organization]: + """ + Read the shared background that guides memory formation across the organization, together with the number of active namespaces. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[Organization] + Organization settings retrieved successfully + """ + _response = self._client_wrapper.httpx_client.request( + "organization", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Organization, + construct_type( + type_=Organization, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def update( + self, + *, + organizational_context: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[Organization]: + """ + Give Supermemory organization-wide background that improves how content is interpreted across every namespace. Send null to remove the existing context. + + Parameters + ---------- + organizational_context : typing.Optional[str] + Shared background that guides memory formation across every namespace. Set null to clear it. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[Organization] + Organization settings updated successfully + """ + _response = self._client_wrapper.httpx_client.request( + "organization", + method="PATCH", + json={ + "organizationalContext": organizational_context, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Organization, + construct_type( + type_=Organization, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawOrganizationClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get( + self, + *, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[Organization]: + """ + Read the shared background that guides memory formation across the organization, together with the number of active namespaces. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[Organization] + Organization settings retrieved successfully + """ + _response = await self._client_wrapper.httpx_client.request( + "organization", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Organization, + construct_type( + type_=Organization, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def update( + self, + *, + organizational_context: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[Organization]: + """ + Give Supermemory organization-wide background that improves how content is interpreted across every namespace. Send null to remove the existing context. + + Parameters + ---------- + organizational_context : typing.Optional[str] + Shared background that guides memory formation across every namespace. Set null to clear it. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[Organization] + Organization settings updated successfully + """ + _response = await self._client_wrapper.httpx_client.request( + "organization", + method="PATCH", + json={ + "organizationalContext": organizational_context, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Organization, + construct_type( + type_=Organization, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/supermemory/profiles/__init__.py b/src/supermemory/profiles/__init__.py new file mode 100644 index 00000000..5cde0202 --- /dev/null +++ b/src/supermemory/profiles/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/supermemory/profiles/client.py b/src/supermemory/profiles/client.py new file mode 100644 index 00000000..fe52aab5 --- /dev/null +++ b/src/supermemory/profiles/client.py @@ -0,0 +1,329 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.profile_buckets import ProfileBuckets +from .raw_client import AsyncRawProfilesClient, RawProfilesClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class ProfilesClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawProfilesClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawProfilesClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawProfilesClient + """ + return self._raw_client + + def get_buckets( + self, + namespace: str, + *, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> ProfileBuckets: + """ + Inspect the profile taxonomy active in this namespace. The response combines organization-wide buckets with namespace-owned additions as a concise name-to-description map. + + Parameters + ---------- + namespace : str + The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ProfileBuckets + Effective namespace profile bucket definitions + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.profiles.get_buckets( + namespace="user_alex", + ) + """ + _response = self._raw_client.get_buckets(namespace, request_options=request_options) + return _response.data + + def set_buckets( + self, + namespace: str, + *, + buckets: typing.Dict[str, str], + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> ProfileBuckets: + """ + Teach Supermemory new ways to organize this namespace's profile, or refine how existing namespace-owned buckets are classified. Omitted buckets remain unchanged and organization-wide buckets stay protected. + + Parameters + ---------- + namespace : str + The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + buckets : typing.Dict[str, str] + Namespace-owned bucket names mapped to descriptions that guide memory classification. Existing names are updated; new names are added. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ProfileBuckets + Updated effective namespace profile bucket definitions + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.profiles.set_buckets( + namespace="user_alex", + buckets={"interests": "Topics the subject actively follows"}, + ) + """ + _response = self._raw_client.set_buckets(namespace, buckets=buckets, request_options=request_options) + return _response.data + + def delete_buckets( + self, + namespace: str, + *, + buckets: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> ProfileBuckets: + """ + Remove profile categories created specifically for this namespace. Unknown names are safely ignored, while organization-wide buckets remain protected. + + Parameters + ---------- + namespace : str + The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + buckets : typing.Sequence[str] + Namespace-owned bucket names to delete. Organization-level buckets are protected and cannot be removed here. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ProfileBuckets + Updated effective namespace profile bucket definitions + + Examples + -------- + from supermemory import Supermemory + + client = Supermemory( + api_key="YOUR_API_KEY", + ) + client.profiles.delete_buckets( + namespace="user_alex", + buckets=["interests"], + ) + """ + _response = self._raw_client.delete_buckets(namespace, buckets=buckets, request_options=request_options) + return _response.data + + +class AsyncProfilesClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawProfilesClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawProfilesClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawProfilesClient + """ + return self._raw_client + + async def get_buckets( + self, + namespace: str, + *, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> ProfileBuckets: + """ + Inspect the profile taxonomy active in this namespace. The response combines organization-wide buckets with namespace-owned additions as a concise name-to-description map. + + Parameters + ---------- + namespace : str + The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ProfileBuckets + Effective namespace profile bucket definitions + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.profiles.get_buckets( + namespace="user_alex", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_buckets(namespace, request_options=request_options) + return _response.data + + async def set_buckets( + self, + namespace: str, + *, + buckets: typing.Dict[str, str], + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> ProfileBuckets: + """ + Teach Supermemory new ways to organize this namespace's profile, or refine how existing namespace-owned buckets are classified. Omitted buckets remain unchanged and organization-wide buckets stay protected. + + Parameters + ---------- + namespace : str + The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + buckets : typing.Dict[str, str] + Namespace-owned bucket names mapped to descriptions that guide memory classification. Existing names are updated; new names are added. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ProfileBuckets + Updated effective namespace profile bucket definitions + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.profiles.set_buckets( + namespace="user_alex", + buckets={"interests": "Topics the subject actively follows"}, + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.set_buckets(namespace, buckets=buckets, request_options=request_options) + return _response.data + + async def delete_buckets( + self, + namespace: str, + *, + buckets: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> ProfileBuckets: + """ + Remove profile categories created specifically for this namespace. Unknown names are safely ignored, while organization-wide buckets remain protected. + + Parameters + ---------- + namespace : str + The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + buckets : typing.Sequence[str] + Namespace-owned bucket names to delete. Organization-level buckets are protected and cannot be removed here. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ProfileBuckets + Updated effective namespace profile bucket definitions + + Examples + -------- + import asyncio + + from supermemory import AsyncSupermemory + + client = AsyncSupermemory( + api_key="YOUR_API_KEY", + ) + + + async def main() -> None: + await client.profiles.delete_buckets( + namespace="user_alex", + buckets=["interests"], + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete_buckets(namespace, buckets=buckets, request_options=request_options) + return _response.data diff --git a/src/supermemory/profiles/raw_client.py b/src/supermemory/profiles/raw_client.py new file mode 100644 index 00000000..6774430d --- /dev/null +++ b/src/supermemory/profiles/raw_client.py @@ -0,0 +1,711 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import encode_path_param +from ..core.parse_error import ParsingError +from ..core.request_options import RequestOptions +from ..core.unchecked_base_model import construct_type +from ..errors.bad_request_error import BadRequestError +from ..errors.conflict_error import ConflictError +from ..errors.forbidden_error import ForbiddenError +from ..errors.internal_server_error import InternalServerError +from ..errors.not_found_error import NotFoundError +from ..errors.unauthorized_error import UnauthorizedError +from ..types.bad_request_error_body import BadRequestErrorBody +from ..types.error_response import ErrorResponse +from ..types.profile_buckets import ProfileBuckets +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawProfilesClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get_buckets( + self, + namespace: str, + *, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[ProfileBuckets]: + """ + Inspect the profile taxonomy active in this namespace. The response combines organization-wide buckets with namespace-owned additions as a concise name-to-description map. + + Parameters + ---------- + namespace : str + The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ProfileBuckets] + Effective namespace profile bucket definitions + """ + _response = self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/profile/buckets", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProfileBuckets, + construct_type( + type_=ProfileBuckets, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def set_buckets( + self, + namespace: str, + *, + buckets: typing.Dict[str, str], + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[ProfileBuckets]: + """ + Teach Supermemory new ways to organize this namespace's profile, or refine how existing namespace-owned buckets are classified. Omitted buckets remain unchanged and organization-wide buckets stay protected. + + Parameters + ---------- + namespace : str + The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + buckets : typing.Dict[str, str] + Namespace-owned bucket names mapped to descriptions that guide memory classification. Existing names are updated; new names are added. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ProfileBuckets] + Updated effective namespace profile bucket definitions + """ + _response = self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/profile/buckets", + method="PUT", + json={ + "buckets": buckets, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProfileBuckets, + construct_type( + type_=ProfileBuckets, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def delete_buckets( + self, + namespace: str, + *, + buckets: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[ProfileBuckets]: + """ + Remove profile categories created specifically for this namespace. Unknown names are safely ignored, while organization-wide buckets remain protected. + + Parameters + ---------- + namespace : str + The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + buckets : typing.Sequence[str] + Namespace-owned bucket names to delete. Organization-level buckets are protected and cannot be removed here. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ProfileBuckets] + Updated effective namespace profile bucket definitions + """ + _response = self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/profile/buckets", + method="DELETE", + json={ + "buckets": buckets, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProfileBuckets, + construct_type( + type_=ProfileBuckets, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawProfilesClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get_buckets( + self, + namespace: str, + *, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[ProfileBuckets]: + """ + Inspect the profile taxonomy active in this namespace. The response combines organization-wide buckets with namespace-owned additions as a concise name-to-description map. + + Parameters + ---------- + namespace : str + The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ProfileBuckets] + Effective namespace profile bucket definitions + """ + _response = await self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/profile/buckets", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProfileBuckets, + construct_type( + type_=ProfileBuckets, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def set_buckets( + self, + namespace: str, + *, + buckets: typing.Dict[str, str], + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[ProfileBuckets]: + """ + Teach Supermemory new ways to organize this namespace's profile, or refine how existing namespace-owned buckets are classified. Omitted buckets remain unchanged and organization-wide buckets stay protected. + + Parameters + ---------- + namespace : str + The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + buckets : typing.Dict[str, str] + Namespace-owned bucket names mapped to descriptions that guide memory classification. Existing names are updated; new names are added. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ProfileBuckets] + Updated effective namespace profile bucket definitions + """ + _response = await self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/profile/buckets", + method="PUT", + json={ + "buckets": buckets, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProfileBuckets, + construct_type( + type_=ProfileBuckets, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def delete_buckets( + self, + namespace: str, + *, + buckets: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[ProfileBuckets]: + """ + Remove profile categories created specifically for this namespace. Unknown names are safely ignored, while organization-wide buckets remain protected. + + Parameters + ---------- + namespace : str + The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + buckets : typing.Sequence[str] + Namespace-owned bucket names to delete. Organization-level buckets are protected and cannot be removed here. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ProfileBuckets] + Updated effective namespace profile bucket definitions + """ + _response = await self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/profile/buckets", + method="DELETE", + json={ + "buckets": buckets, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProfileBuckets, + construct_type( + type_=ProfileBuckets, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/supermemory/raw_client.py b/src/supermemory/raw_client.py new file mode 100644 index 00000000..ea7aec98 --- /dev/null +++ b/src/supermemory/raw_client.py @@ -0,0 +1,1201 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx +from json.decoder import JSONDecodeError + +from .core.api_error import ApiError +from .core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from .core.http_response import AsyncHttpResponse, HttpResponse +from .core.jsonable_encoder import encode_path_param +from .core.parse_error import ParsingError +from .core.request_options import RequestOptions +from .core.serialization import convert_and_respect_annotation_metadata +from .core.unchecked_base_model import construct_type +from .errors.bad_request_error import BadRequestError +from .errors.conflict_error import ConflictError +from .errors.forbidden_error import ForbiddenError +from .errors.internal_server_error import InternalServerError +from .errors.payment_required_error import PaymentRequiredError +from .errors.unauthorized_error import UnauthorizedError +from .requests.filter_expression import FilterExpressionParams +from .requests.list_request_limit import ListRequestLimitParams +from .requests.list_request_page import ListRequestPageParams +from .requests.metadata_value import MetadataValueParams +from .requests.search_request_attach import SearchRequestAttachParams +from .types.bad_request_error_body import BadRequestErrorBody +from .types.document_ref import DocumentRef +from .types.dreaming_mode import DreamingMode +from .types.error_response import ErrorResponse +from .types.list_response import ListResponse +from .types.list_sort import ListSort +from .types.list_type import ListType +from .types.profile_response import ProfileResponse +from .types.search_mode import SearchMode +from .types.search_request_rerank import SearchRequestRerank +from .types.search_response import SearchResponse +from .types.sort_order import SortOrder +from .types.task_type import TaskType +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawSupermemory: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def add( + self, + namespace: str, + *, + content: str, + task_type: typing.Optional[TaskType] = None, + dreaming: typing.Optional[DreamingMode] = None, + id: typing.Optional[str] = OMIT, + supporting_context: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, MetadataValueParams]] = OMIT, + group: typing.Optional[typing.Dict[str, MetadataValueParams]] = OMIT, + date: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[DocumentRef]: + """ + Turn text or a supported URL into searchable, evolving memory. Supply a new ID to create a document, or reuse an existing ID to append new information while preserving its history. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + content : str + The content to process. This may be plaintext or a URL to supported rich content. + + task_type : typing.Optional[TaskType] + Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + + dreaming : typing.Optional[DreamingMode] + Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + + id : typing.Optional[str] + An optional caller-defined document ID + + supporting_context : typing.Optional[str] + Context used to guide memory extraction within this namespace. Max 1500 characters. + + metadata : typing.Optional[typing.Dict[str, MetadataValueParams]] + Arbitrary metadata attached to the document and derived data + + group : typing.Optional[typing.Dict[str, MetadataValueParams]] + Metadata values that isolate related context within the namespace + + date : typing.Optional[str] + When the source content is from, in ISO 8601 format + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[DocumentRef] + Document accepted for processing + """ + _response = self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/document", + method="POST", + params={ + "taskType": task_type, + "dreaming": dreaming, + }, + json={ + "content": content, + "id": id, + "supportingContext": supporting_context, + "metadata": convert_and_respect_annotation_metadata( + object_=metadata, annotation=typing.Dict[str, MetadataValueParams], direction="write" + ), + "group": convert_and_respect_annotation_metadata( + object_=group, annotation=typing.Dict[str, MetadataValueParams], direction="write" + ), + "date": date, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DocumentRef, + construct_type( + type_=DocumentRef, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def search( + self, + namespace: str, + *, + query: str, + limit: typing.Optional[int] = None, + search_mode: typing.Optional[SearchMode] = None, + filter: typing.Optional[FilterExpressionParams] = OMIT, + attach: typing.Optional[SearchRequestAttachParams] = OMIT, + threshold: typing.Optional[float] = OMIT, + rerank: typing.Optional[SearchRequestRerank] = OMIT, + rewrite_query: typing.Optional[bool] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[SearchResponse]: + """ + Recall the most relevant learned context and source passages from a namespace. Hybrid search combines memories with document chunks by default, with optional query rewriting, reranking, and supporting context attachments. + + Parameters + ---------- + namespace : str + The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + query : str + Natural-language question, topic, or phrase to retrieve relevant context for. Replaces the v4 `q` field. + + limit : typing.Optional[int] + Maximum number of results to return + + search_mode : typing.Optional[SearchMode] + Search surface. "hybrid" combines learned memories with source chunks, "memories" returns learned context, and "chunks" returns source passages. + + filter : typing.Optional[FilterExpressionParams] + Type-safe metadata conditions applied before ranking results. Replaces the v4 `filters` field and unifies both legacy filter formats into one expression type. + + attach : typing.Optional[SearchRequestAttachParams] + Optional context to include alongside each matching result + + threshold : typing.Optional[float] + Minimum relevance score from 0 to 1. Raise it for precision (fewer, accurate results) or lower it for broader recall (more results). + + rerank : typing.Optional[SearchRequestRerank] + Post-retrieval ranking. "order" improves result ordering; "aggregate" also combines overlapping context into cleaner answers. This is helpful if you want to ensure the most relevant results are returned. + + rewrite_query : typing.Optional[bool] + Expand and clarify the query before retrieval to improve recall for conversational or underspecified prompts. This increases the latency by about 400ms. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[SearchResponse] + Search results + """ + _response = self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/search", + method="POST", + params={ + "limit": limit, + "searchMode": search_mode, + }, + json={ + "query": query, + "filter": convert_and_respect_annotation_metadata( + object_=filter, annotation=FilterExpressionParams, direction="write" + ), + "attach": convert_and_respect_annotation_metadata( + object_=attach, annotation=SearchRequestAttachParams, direction="write" + ), + "threshold": threshold, + "rerank": rerank, + "rewriteQuery": rewrite_query, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SearchResponse, + construct_type( + type_=SearchResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def profile( + self, + namespace: str, + *, + filter: typing.Optional[FilterExpressionParams] = OMIT, + buckets: typing.Optional[typing.Sequence[str]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[ProfileResponse]: + """ + Read a continuously maintained understanding of the subject represented by this namespace. Stable facts, evolving context, and selected custom buckets are returned together without requiring a search query. + + Parameters + ---------- + namespace : str + The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + filter : typing.Optional[FilterExpressionParams] + Type-safe metadata conditions that limit which memories contribute to the profile + + buckets : typing.Optional[typing.Sequence[str]] + Custom buckets to return. Omit to return every effective bucket. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ProfileResponse] + Namespace profile. Send `Accept: text/markdown` to receive it as a markdown document instead of JSON. + """ + _response = self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/profile", + method="POST", + json={ + "filter": convert_and_respect_annotation_metadata( + object_=filter, annotation=FilterExpressionParams, direction="write" + ), + "buckets": buckets, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProfileResponse, + construct_type( + type_=ProfileResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def list( + self, + namespace: str, + type: ListType, + *, + page: typing.Optional[ListRequestPageParams] = None, + limit: typing.Optional[ListRequestLimitParams] = None, + sort: typing.Optional[ListSort] = None, + order: typing.Optional[SortOrder] = None, + filter: typing.Optional[FilterExpressionParams] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> HttpResponse[ListResponse]: + """ + Browse documents, source chunks, or learned memories through one predictable paginated contract. Choose the collection in the path; the other collection arrays remain empty for a stable response shape. + + Parameters + ---------- + namespace : str + Namespace containing the resources to list. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope resources. + + type : ListType + Resource collection to list: source documents, extracted chunks, or learned memories + + page : typing.Optional[ListRequestPageParams] + One-based page number + + limit : typing.Optional[ListRequestLimitParams] + Maximum resources to return per page + + sort : typing.Optional[ListSort] + Field used to order results. Position is available only when listing chunks. + + order : typing.Optional[SortOrder] + Ascending or descending sort direction + + filter : typing.Optional[FilterExpressionParams] + Type-safe metadata conditions applied before pagination + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ListResponse] + Namespace resources retrieved successfully + """ + _response = self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/list/{encode_path_param(type)}", + method="POST", + params={ + "page": convert_and_respect_annotation_metadata( + object_=page, annotation=ListRequestPageParams, direction="write" + ), + "limit": convert_and_respect_annotation_metadata( + object_=limit, annotation=ListRequestLimitParams, direction="write" + ), + "sort": sort, + "order": order, + }, + json={ + "filter": convert_and_respect_annotation_metadata( + object_=filter, annotation=FilterExpressionParams, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListResponse, + construct_type( + type_=ListResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawSupermemory: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def add( + self, + namespace: str, + *, + content: str, + task_type: typing.Optional[TaskType] = None, + dreaming: typing.Optional[DreamingMode] = None, + id: typing.Optional[str] = OMIT, + supporting_context: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Dict[str, MetadataValueParams]] = OMIT, + group: typing.Optional[typing.Dict[str, MetadataValueParams]] = OMIT, + date: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[DocumentRef]: + """ + Turn text or a supported URL into searchable, evolving memory. Supply a new ID to create a document, or reuse an existing ID to append new information while preserving its history. + + Parameters + ---------- + namespace : str + The isolated namespace. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope documents. + + content : str + The content to process. This may be plaintext or a URL to supported rich content. + + task_type : typing.Optional[TaskType] + Processing pipeline. "memory" builds durable learned context; "superrag" optimizes the document for retrieval without generating memories. + + dreaming : typing.Optional[DreamingMode] + Processing mode. "dynamic" (default) groups related documents so memories form from coherent context. "instant" processes each document independently right away and bills one extra operation per document. + + id : typing.Optional[str] + An optional caller-defined document ID + + supporting_context : typing.Optional[str] + Context used to guide memory extraction within this namespace. Max 1500 characters. + + metadata : typing.Optional[typing.Dict[str, MetadataValueParams]] + Arbitrary metadata attached to the document and derived data + + group : typing.Optional[typing.Dict[str, MetadataValueParams]] + Metadata values that isolate related context within the namespace + + date : typing.Optional[str] + When the source content is from, in ISO 8601 format + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[DocumentRef] + Document accepted for processing + """ + _response = await self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/document", + method="POST", + params={ + "taskType": task_type, + "dreaming": dreaming, + }, + json={ + "content": content, + "id": id, + "supportingContext": supporting_context, + "metadata": convert_and_respect_annotation_metadata( + object_=metadata, annotation=typing.Dict[str, MetadataValueParams], direction="write" + ), + "group": convert_and_respect_annotation_metadata( + object_=group, annotation=typing.Dict[str, MetadataValueParams], direction="write" + ), + "date": date, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DocumentRef, + construct_type( + type_=DocumentRef, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def search( + self, + namespace: str, + *, + query: str, + limit: typing.Optional[int] = None, + search_mode: typing.Optional[SearchMode] = None, + filter: typing.Optional[FilterExpressionParams] = OMIT, + attach: typing.Optional[SearchRequestAttachParams] = OMIT, + threshold: typing.Optional[float] = OMIT, + rerank: typing.Optional[SearchRequestRerank] = OMIT, + rewrite_query: typing.Optional[bool] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[SearchResponse]: + """ + Recall the most relevant learned context and source passages from a namespace. Hybrid search combines memories with document chunks by default, with optional query rewriting, reranking, and supporting context attachments. + + Parameters + ---------- + namespace : str + The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + query : str + Natural-language question, topic, or phrase to retrieve relevant context for. Replaces the v4 `q` field. + + limit : typing.Optional[int] + Maximum number of results to return + + search_mode : typing.Optional[SearchMode] + Search surface. "hybrid" combines learned memories with source chunks, "memories" returns learned context, and "chunks" returns source passages. + + filter : typing.Optional[FilterExpressionParams] + Type-safe metadata conditions applied before ranking results. Replaces the v4 `filters` field and unifies both legacy filter formats into one expression type. + + attach : typing.Optional[SearchRequestAttachParams] + Optional context to include alongside each matching result + + threshold : typing.Optional[float] + Minimum relevance score from 0 to 1. Raise it for precision (fewer, accurate results) or lower it for broader recall (more results). + + rerank : typing.Optional[SearchRequestRerank] + Post-retrieval ranking. "order" improves result ordering; "aggregate" also combines overlapping context into cleaner answers. This is helpful if you want to ensure the most relevant results are returned. + + rewrite_query : typing.Optional[bool] + Expand and clarify the query before retrieval to improve recall for conversational or underspecified prompts. This increases the latency by about 400ms. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[SearchResponse] + Search results + """ + _response = await self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/search", + method="POST", + params={ + "limit": limit, + "searchMode": search_mode, + }, + json={ + "query": query, + "filter": convert_and_respect_annotation_metadata( + object_=filter, annotation=FilterExpressionParams, direction="write" + ), + "attach": convert_and_respect_annotation_metadata( + object_=attach, annotation=SearchRequestAttachParams, direction="write" + ), + "threshold": threshold, + "rerank": rerank, + "rewriteQuery": rewrite_query, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SearchResponse, + construct_type( + type_=SearchResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def profile( + self, + namespace: str, + *, + filter: typing.Optional[FilterExpressionParams] = OMIT, + buckets: typing.Optional[typing.Sequence[str]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[ProfileResponse]: + """ + Read a continuously maintained understanding of the subject represented by this namespace. Stable facts, evolving context, and selected custom buckets are returned together without requiring a search query. + + Parameters + ---------- + namespace : str + The isolated namespace to search. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope memories. + + filter : typing.Optional[FilterExpressionParams] + Type-safe metadata conditions that limit which memories contribute to the profile + + buckets : typing.Optional[typing.Sequence[str]] + Custom buckets to return. Omit to return every effective bucket. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ProfileResponse] + Namespace profile. Send `Accept: text/markdown` to receive it as a markdown document instead of JSON. + """ + _response = await self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/profile", + method="POST", + json={ + "filter": convert_and_respect_annotation_metadata( + object_=filter, annotation=FilterExpressionParams, direction="write" + ), + "buckets": buckets, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProfileResponse, + construct_type( + type_=ProfileResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def list( + self, + namespace: str, + type: ListType, + *, + page: typing.Optional[ListRequestPageParams] = None, + limit: typing.Optional[ListRequestLimitParams] = None, + sort: typing.Optional[ListSort] = None, + order: typing.Optional[SortOrder] = None, + filter: typing.Optional[FilterExpressionParams] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + extra_headers: typing.Optional[typing.Mapping[str, str]] = None, + extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, + extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, + timeout: typing.Union[float, httpx.Timeout, None] = None, + ) -> AsyncHttpResponse[ListResponse]: + """ + Browse documents, source chunks, or learned memories through one predictable paginated contract. Choose the collection in the path; the other collection arrays remain empty for a stable response shape. + + Parameters + ---------- + namespace : str + Namespace containing the resources to list. This can be an ID for your user, a project ID, or any other identifier you wish to use to scope resources. + + type : ListType + Resource collection to list: source documents, extracted chunks, or learned memories + + page : typing.Optional[ListRequestPageParams] + One-based page number + + limit : typing.Optional[ListRequestLimitParams] + Maximum resources to return per page + + sort : typing.Optional[ListSort] + Field used to order results. Position is available only when listing chunks. + + order : typing.Optional[SortOrder] + Ascending or descending sort direction + + filter : typing.Optional[FilterExpressionParams] + Type-safe metadata conditions applied before pagination + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ListResponse] + Namespace resources retrieved successfully + """ + _response = await self._client_wrapper.httpx_client.request( + f"ns/{encode_path_param(namespace)}/list/{encode_path_param(type)}", + method="POST", + params={ + "page": convert_and_respect_annotation_metadata( + object_=page, annotation=ListRequestPageParams, direction="write" + ), + "limit": convert_and_respect_annotation_metadata( + object_=limit, annotation=ListRequestLimitParams, direction="write" + ), + "sort": sort, + "order": order, + }, + json={ + "filter": convert_and_respect_annotation_metadata( + object_=filter, annotation=FilterExpressionParams, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ListResponse, + construct_type( + type_=ListResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + BadRequestErrorBody, + construct_type( + type_=BadRequestErrorBody, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + headers=dict(_response.headers), + body=typing.cast( + ErrorResponse, + construct_type( + type_=ErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/supermemory/requests/__init__.py b/src/supermemory/requests/__init__.py new file mode 100644 index 00000000..4ad14415 --- /dev/null +++ b/src/supermemory/requests/__init__.py @@ -0,0 +1,307 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .bad_request_error_body import BadRequestErrorBodyParams + from .batch_add_item_result import BatchAddItemResultParams + from .batch_add_result import BatchAddResultParams + from .chunk import ChunkParams + from .chunk_list_item import ChunkListItemParams + from .chunk_system import ChunkSystemParams + from .connector import ConnectorParams + from .connector_capabilities import ConnectorCapabilitiesParams + from .connector_config_value import ConnectorConfigValueParams + from .connector_deleted import ConnectorDeletedParams + from .connector_last_sync import ConnectorLastSyncParams + from .connector_list import ConnectorListParams + from .connector_picker import ConnectorPickerParams + from .connector_selection import ConnectorSelectionParams + from .connector_selection_item import ConnectorSelectionItemParams + from .connector_setup import ( + ConnectorSetupParams, + ConnectorSetup_GithubParams, + ConnectorSetup_GmailParams, + ConnectorSetup_GoogleDriveParams, + ConnectorSetup_GranolaParams, + ConnectorSetup_NotionParams, + ConnectorSetup_OnedriveParams, + ConnectorSetup_S3Params, + ConnectorSetup_WebCrawlerParams, + ) + from .connector_setup_github import ConnectorSetupGithubParams + from .connector_setup_gmail import ConnectorSetupGmailParams + from .connector_setup_google_drive import ConnectorSetupGoogleDriveParams + from .connector_setup_granola import ConnectorSetupGranolaParams + from .connector_setup_notion import ConnectorSetupNotionParams + from .connector_setup_onedrive import ConnectorSetupOnedriveParams + from .connector_setup_result import ConnectorSetupResultParams + from .connector_setup_s3 import ConnectorSetupS3Params + from .connector_setup_web_crawler import ConnectorSetupWebCrawlerParams + from .connector_sync import ConnectorSyncParams + from .connector_sync_failure import ConnectorSyncFailureParams + from .connector_sync_started import ConnectorSyncStartedParams + from .delete_document_error import DeleteDocumentErrorParams + from .delete_documents_result import DeleteDocumentsResultParams + from .document import DocumentParams + from .document_input import DocumentInputParams + from .document_list_item import DocumentListItemParams + from .document_ref import DocumentRefParams + from .document_system import DocumentSystemParams + from .error_response import ErrorResponseParams + from .file_upload_result import FileUploadResultParams + from .filter_and import FilterAndParams + from .filter_array_contains import FilterArrayContainsParams + from .filter_expression import FilterExpressionParams + from .filter_number_compare import FilterNumberCompareParams + from .filter_or import FilterOrParams + from .filter_predicate import FilterPredicateParams + from .filter_string_contains import FilterStringContainsParams + from .filter_string_equals import FilterStringEqualsParams + from .filter_value_equals import FilterValueEqualsParams + from .filter_value_equals_value import FilterValueEqualsValueParams + from .forget_error import ForgetErrorParams + from .forget_match import ForgetMatchParams + from .forget_result import ForgetResultParams + from .google_drive_config import GoogleDriveConfigParams + from .granola_config import GranolaConfigParams + from .list_request_limit import ListRequestLimitParams + from .list_request_page import ListRequestPageParams + from .list_response import ListResponseParams + from .memory import MemoryParams + from .memory_system import MemorySystemParams + from .metadata_value import MetadataValueParams + from .namespace import NamespaceParams + from .namespace_deleted import NamespaceDeletedParams + from .namespace_details import NamespaceDetailsParams + from .namespace_system import NamespaceSystemParams + from .organization import OrganizationParams + from .pagination import PaginationParams + from .profile import ProfileParams + from .profile_buckets import ProfileBucketsParams + from .profile_fact import ProfileFactParams + from .profile_response import ProfileResponseParams + from .related_memories import RelatedMemoriesParams + from .related_memory import RelatedMemoryParams + from .related_memory_system import RelatedMemorySystemParams + from .s3config import S3ConfigParams + from .search_request_attach import SearchRequestAttachParams + from .search_response import SearchResponseParams + from .search_result import SearchResultParams + from .search_result_included import SearchResultIncludedParams + from .search_result_included_document import SearchResultIncludedDocumentParams + from .search_result_system import SearchResultSystemParams + from .validation_error_response import ValidationErrorResponseParams + from .validation_error_response_error_item import ValidationErrorResponseErrorItemParams + from .validation_error_response_error_item_path_item import ValidationErrorResponseErrorItemPathItemParams + from .web_crawler_config import WebCrawlerConfigParams +_dynamic_imports: typing.Dict[str, str] = { + "BadRequestErrorBodyParams": ".bad_request_error_body", + "BatchAddItemResultParams": ".batch_add_item_result", + "BatchAddResultParams": ".batch_add_result", + "ChunkListItemParams": ".chunk_list_item", + "ChunkParams": ".chunk", + "ChunkSystemParams": ".chunk_system", + "ConnectorCapabilitiesParams": ".connector_capabilities", + "ConnectorConfigValueParams": ".connector_config_value", + "ConnectorDeletedParams": ".connector_deleted", + "ConnectorLastSyncParams": ".connector_last_sync", + "ConnectorListParams": ".connector_list", + "ConnectorParams": ".connector", + "ConnectorPickerParams": ".connector_picker", + "ConnectorSelectionItemParams": ".connector_selection_item", + "ConnectorSelectionParams": ".connector_selection", + "ConnectorSetupGithubParams": ".connector_setup_github", + "ConnectorSetupGmailParams": ".connector_setup_gmail", + "ConnectorSetupGoogleDriveParams": ".connector_setup_google_drive", + "ConnectorSetupGranolaParams": ".connector_setup_granola", + "ConnectorSetupNotionParams": ".connector_setup_notion", + "ConnectorSetupOnedriveParams": ".connector_setup_onedrive", + "ConnectorSetupParams": ".connector_setup", + "ConnectorSetupResultParams": ".connector_setup_result", + "ConnectorSetupS3Params": ".connector_setup_s3", + "ConnectorSetupWebCrawlerParams": ".connector_setup_web_crawler", + "ConnectorSetup_GithubParams": ".connector_setup", + "ConnectorSetup_GmailParams": ".connector_setup", + "ConnectorSetup_GoogleDriveParams": ".connector_setup", + "ConnectorSetup_GranolaParams": ".connector_setup", + "ConnectorSetup_NotionParams": ".connector_setup", + "ConnectorSetup_OnedriveParams": ".connector_setup", + "ConnectorSetup_S3Params": ".connector_setup", + "ConnectorSetup_WebCrawlerParams": ".connector_setup", + "ConnectorSyncFailureParams": ".connector_sync_failure", + "ConnectorSyncParams": ".connector_sync", + "ConnectorSyncStartedParams": ".connector_sync_started", + "DeleteDocumentErrorParams": ".delete_document_error", + "DeleteDocumentsResultParams": ".delete_documents_result", + "DocumentInputParams": ".document_input", + "DocumentListItemParams": ".document_list_item", + "DocumentParams": ".document", + "DocumentRefParams": ".document_ref", + "DocumentSystemParams": ".document_system", + "ErrorResponseParams": ".error_response", + "FileUploadResultParams": ".file_upload_result", + "FilterAndParams": ".filter_and", + "FilterArrayContainsParams": ".filter_array_contains", + "FilterExpressionParams": ".filter_expression", + "FilterNumberCompareParams": ".filter_number_compare", + "FilterOrParams": ".filter_or", + "FilterPredicateParams": ".filter_predicate", + "FilterStringContainsParams": ".filter_string_contains", + "FilterStringEqualsParams": ".filter_string_equals", + "FilterValueEqualsParams": ".filter_value_equals", + "FilterValueEqualsValueParams": ".filter_value_equals_value", + "ForgetErrorParams": ".forget_error", + "ForgetMatchParams": ".forget_match", + "ForgetResultParams": ".forget_result", + "GoogleDriveConfigParams": ".google_drive_config", + "GranolaConfigParams": ".granola_config", + "ListRequestLimitParams": ".list_request_limit", + "ListRequestPageParams": ".list_request_page", + "ListResponseParams": ".list_response", + "MemoryParams": ".memory", + "MemorySystemParams": ".memory_system", + "MetadataValueParams": ".metadata_value", + "NamespaceDeletedParams": ".namespace_deleted", + "NamespaceDetailsParams": ".namespace_details", + "NamespaceParams": ".namespace", + "NamespaceSystemParams": ".namespace_system", + "OrganizationParams": ".organization", + "PaginationParams": ".pagination", + "ProfileBucketsParams": ".profile_buckets", + "ProfileFactParams": ".profile_fact", + "ProfileParams": ".profile", + "ProfileResponseParams": ".profile_response", + "RelatedMemoriesParams": ".related_memories", + "RelatedMemoryParams": ".related_memory", + "RelatedMemorySystemParams": ".related_memory_system", + "S3ConfigParams": ".s3config", + "SearchRequestAttachParams": ".search_request_attach", + "SearchResponseParams": ".search_response", + "SearchResultIncludedDocumentParams": ".search_result_included_document", + "SearchResultIncludedParams": ".search_result_included", + "SearchResultParams": ".search_result", + "SearchResultSystemParams": ".search_result_system", + "ValidationErrorResponseErrorItemParams": ".validation_error_response_error_item", + "ValidationErrorResponseErrorItemPathItemParams": ".validation_error_response_error_item_path_item", + "ValidationErrorResponseParams": ".validation_error_response", + "WebCrawlerConfigParams": ".web_crawler_config", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "BadRequestErrorBodyParams", + "BatchAddItemResultParams", + "BatchAddResultParams", + "ChunkListItemParams", + "ChunkParams", + "ChunkSystemParams", + "ConnectorCapabilitiesParams", + "ConnectorConfigValueParams", + "ConnectorDeletedParams", + "ConnectorLastSyncParams", + "ConnectorListParams", + "ConnectorParams", + "ConnectorPickerParams", + "ConnectorSelectionItemParams", + "ConnectorSelectionParams", + "ConnectorSetupGithubParams", + "ConnectorSetupGmailParams", + "ConnectorSetupGoogleDriveParams", + "ConnectorSetupGranolaParams", + "ConnectorSetupNotionParams", + "ConnectorSetupOnedriveParams", + "ConnectorSetupParams", + "ConnectorSetupResultParams", + "ConnectorSetupS3Params", + "ConnectorSetupWebCrawlerParams", + "ConnectorSetup_GithubParams", + "ConnectorSetup_GmailParams", + "ConnectorSetup_GoogleDriveParams", + "ConnectorSetup_GranolaParams", + "ConnectorSetup_NotionParams", + "ConnectorSetup_OnedriveParams", + "ConnectorSetup_S3Params", + "ConnectorSetup_WebCrawlerParams", + "ConnectorSyncFailureParams", + "ConnectorSyncParams", + "ConnectorSyncStartedParams", + "DeleteDocumentErrorParams", + "DeleteDocumentsResultParams", + "DocumentInputParams", + "DocumentListItemParams", + "DocumentParams", + "DocumentRefParams", + "DocumentSystemParams", + "ErrorResponseParams", + "FileUploadResultParams", + "FilterAndParams", + "FilterArrayContainsParams", + "FilterExpressionParams", + "FilterNumberCompareParams", + "FilterOrParams", + "FilterPredicateParams", + "FilterStringContainsParams", + "FilterStringEqualsParams", + "FilterValueEqualsParams", + "FilterValueEqualsValueParams", + "ForgetErrorParams", + "ForgetMatchParams", + "ForgetResultParams", + "GoogleDriveConfigParams", + "GranolaConfigParams", + "ListRequestLimitParams", + "ListRequestPageParams", + "ListResponseParams", + "MemoryParams", + "MemorySystemParams", + "MetadataValueParams", + "NamespaceDeletedParams", + "NamespaceDetailsParams", + "NamespaceParams", + "NamespaceSystemParams", + "OrganizationParams", + "PaginationParams", + "ProfileBucketsParams", + "ProfileFactParams", + "ProfileParams", + "ProfileResponseParams", + "RelatedMemoriesParams", + "RelatedMemoryParams", + "RelatedMemorySystemParams", + "S3ConfigParams", + "SearchRequestAttachParams", + "SearchResponseParams", + "SearchResultIncludedDocumentParams", + "SearchResultIncludedParams", + "SearchResultParams", + "SearchResultSystemParams", + "ValidationErrorResponseErrorItemParams", + "ValidationErrorResponseErrorItemPathItemParams", + "ValidationErrorResponseParams", + "WebCrawlerConfigParams", +] diff --git a/src/supermemory/requests/bad_request_error_body.py b/src/supermemory/requests/bad_request_error_body.py new file mode 100644 index 00000000..18114352 --- /dev/null +++ b/src/supermemory/requests/bad_request_error_body.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .error_response import ErrorResponseParams +from .validation_error_response import ValidationErrorResponseParams + +BadRequestErrorBodyParams = typing.Union[ErrorResponseParams, ValidationErrorResponseParams] diff --git a/src/supermemory/requests/batch_add_item_result.py b/src/supermemory/requests/batch_add_item_result.py new file mode 100644 index 00000000..69b4ecf7 --- /dev/null +++ b/src/supermemory/requests/batch_add_item_result.py @@ -0,0 +1,30 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + + +class BatchAddItemResultParams(typing_extensions.TypedDict): + id: str + """ + Unique identifier of the document (empty string for failed items) + """ + + status: str + """ + Status of the document (e.g. 'done', 'queued', 'error') + """ + + error: typing_extensions.NotRequired[str] + """ + Error message when status is 'error' + """ + + details: typing_extensions.NotRequired[str] + """ + Additional error details when status is 'error' + """ + + url: typing_extensions.NotRequired[str] + """ + URL of the failed item, when it had one. Only present on failed items, where it may be the only way to tell which input the error belongs to (the id falls back to 'unknown' when the input had neither an id nor a customId). + """ diff --git a/src/supermemory/requests/batch_add_result.py b/src/supermemory/requests/batch_add_result.py new file mode 100644 index 00000000..4c7313a2 --- /dev/null +++ b/src/supermemory/requests/batch_add_result.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from .batch_add_item_result import BatchAddItemResultParams + + +class BatchAddResultParams(typing_extensions.TypedDict): + results: typing.Sequence[BatchAddItemResultParams] + """ + Array of results for each document in the batch + """ + + failed: float + """ + Count of documents that failed to add + """ + + success: float + """ + Count of documents successfully added + """ diff --git a/src/supermemory/requests/chunk.py b/src/supermemory/requests/chunk.py new file mode 100644 index 00000000..bd961a01 --- /dev/null +++ b/src/supermemory/requests/chunk.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from .chunk_system import ChunkSystemParams + + +class ChunkParams(typing_extensions.TypedDict): + """ + A chunk of source content belonging to a document + """ + + id: str + """ + Stable chunk identifier + """ + + position: int + """ + Zero-based order within the document + """ + + content: str + """ + Chunk text used for retrieval + """ + + type: str + """ + Chunk content type + """ + + metadata: typing.Optional[typing.Dict[str, typing.Any]] + """ + Chunk metadata, when available + """ + + system: ChunkSystemParams + """ + Lifecycle timestamps maintained by Supermemory + """ diff --git a/src/supermemory/requests/chunk_list_item.py b/src/supermemory/requests/chunk_list_item.py new file mode 100644 index 00000000..7f324f55 --- /dev/null +++ b/src/supermemory/requests/chunk_list_item.py @@ -0,0 +1,44 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata +from .chunk_system import ChunkSystemParams + + +class ChunkListItemParams(typing_extensions.TypedDict): + id: str + """ + Stable chunk identifier + """ + + position: int + """ + Zero-based order within the document + """ + + content: str + """ + Chunk text used for retrieval + """ + + type: str + """ + Chunk content type + """ + + metadata: typing.Optional[typing.Dict[str, typing.Any]] + """ + Chunk metadata, when available + """ + + system: ChunkSystemParams + """ + Lifecycle timestamps maintained by Supermemory + """ + + document_id: typing_extensions.Annotated[str, FieldMetadata(alias="documentId")] + """ + ID of the chunk's parent document + """ diff --git a/src/supermemory/requests/chunk_system.py b/src/supermemory/requests/chunk_system.py new file mode 100644 index 00000000..0740e49c --- /dev/null +++ b/src/supermemory/requests/chunk_system.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class ChunkSystemParams(typing_extensions.TypedDict): + """ + Lifecycle timestamps maintained by Supermemory + """ + + created_at: typing_extensions.Annotated[dt.datetime, FieldMetadata(alias="createdAt")] + """ + ISO 8601 timestamp when the chunk was created + """ diff --git a/src/supermemory/requests/connector.py b/src/supermemory/requests/connector.py new file mode 100644 index 00000000..d259aa7b --- /dev/null +++ b/src/supermemory/requests/connector.py @@ -0,0 +1,84 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata +from ..types.connector_provider import ConnectorProvider +from ..types.connector_status import ConnectorStatus +from .connector_capabilities import ConnectorCapabilitiesParams +from .connector_config_value import ConnectorConfigValueParams +from .connector_last_sync import ConnectorLastSyncParams +from .connector_picker import ConnectorPickerParams +from .connector_selection_item import ConnectorSelectionItemParams +from .connector_sync import ConnectorSyncParams + + +class ConnectorParams(typing_extensions.TypedDict): + id: str + provider: ConnectorProvider + """ + External source + """ + + status: ConnectorStatus + """ + pending_authorization until the user finishes the provider login at authUrl; DELETE cancels it. Pending connectors are not listed. + """ + + auth_url: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="authUrl")]] + """ + Present while pending, when the caller can create connectors in this namespace: where to send the user to authorize + """ + + auth_url_expires_at: typing_extensions.NotRequired[ + typing_extensions.Annotated[dt.datetime, FieldMetadata(alias="authUrlExpiresAt")] + ] + """ + Present while pending: when authUrl stops working + """ + + namespace: typing.Optional[str] + """ + Namespace the connector syncs into. Null only for connectors created before namespaces were required. + """ + + account: typing.Optional[str] + """ + Account on the provider side, usually an email + """ + + capabilities: ConnectorCapabilitiesParams + config: typing.Optional[typing.Dict[str, typing.Optional[ConnectorConfigValueParams]]] + """ + Non-secret setup, such as the S3 bucket, crawler start URL or Notion workspace. Credentials are never returned. + """ + + selection: typing.Optional[typing.Dict[str, typing.Optional[typing.Sequence[ConnectorSelectionItemParams]]]] + """ + What syncs, grouped by kind, in the same shape PATCH takes. Null for connectors that sync everything. + """ + + document_limit: typing_extensions.Annotated[float, FieldMetadata(alias="documentLimit")] + document_count: typing_extensions.Annotated[float, FieldMetadata(alias="documentCount")] + last_sync: typing_extensions.Annotated[typing.Optional[ConnectorLastSyncParams], FieldMetadata(alias="lastSync")] + """ + Most recent sync run + """ + + last_synced_at: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="lastSyncedAt")] + """ + When content last finished syncing + """ + + created_at: typing_extensions.Annotated[dt.datetime, FieldMetadata(alias="createdAt")] + syncs: typing_extensions.NotRequired[typing.Sequence[ConnectorSyncParams]] + """ + Present when attach=syncs + """ + + picker: typing_extensions.NotRequired[typing.Optional[ConnectorPickerParams]] + """ + Present when attach=picker. Null for connectors that sync everything. + """ diff --git a/src/supermemory/requests/connector_capabilities.py b/src/supermemory/requests/connector_capabilities.py new file mode 100644 index 00000000..b66bf940 --- /dev/null +++ b/src/supermemory/requests/connector_capabilities.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + + +class ConnectorCapabilitiesParams(typing_extensions.TypedDict): + selectable: bool + """ + Supports choosing what to sync via the hosted picker + """ + + webhooks: bool + """ + Receives change notifications instead of only polling + """ diff --git a/src/supermemory/requests/connector_config_value.py b/src/supermemory/requests/connector_config_value.py new file mode 100644 index 00000000..d004a9d6 --- /dev/null +++ b/src/supermemory/requests/connector_config_value.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +ConnectorConfigValueParams = typing.Union[str, float, bool] diff --git a/src/supermemory/requests/connector_deleted.py b/src/supermemory/requests/connector_deleted.py new file mode 100644 index 00000000..c28cfac0 --- /dev/null +++ b/src/supermemory/requests/connector_deleted.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions + + +class ConnectorDeletedParams(typing_extensions.TypedDict): + id: str + deleted: typing.Literal[True] diff --git a/src/supermemory/requests/connector_last_sync.py b/src/supermemory/requests/connector_last_sync.py new file mode 100644 index 00000000..1a15d3fc --- /dev/null +++ b/src/supermemory/requests/connector_last_sync.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata +from ..types.connector_error_code import ConnectorErrorCode +from ..types.connector_sync_status import ConnectorSyncStatus + + +class ConnectorLastSyncParams(typing_extensions.TypedDict): + status: ConnectorSyncStatus + error_code: typing_extensions.Annotated[typing.Optional[ConnectorErrorCode], FieldMetadata(alias="errorCode")] + error: typing.Optional[str] + """ + Customer-safe error message + """ + + started_at: typing_extensions.Annotated[dt.datetime, FieldMetadata(alias="startedAt")] + completed_at: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="completedAt")] diff --git a/src/supermemory/requests/connector_list.py b/src/supermemory/requests/connector_list.py new file mode 100644 index 00000000..01451df5 --- /dev/null +++ b/src/supermemory/requests/connector_list.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from .connector import ConnectorParams +from .pagination import PaginationParams + + +class ConnectorListParams(typing_extensions.TypedDict): + connectors: typing.Sequence[ConnectorParams] + pagination: PaginationParams + """ + Pagination metadata + """ diff --git a/src/supermemory/requests/connector_picker.py b/src/supermemory/requests/connector_picker.py new file mode 100644 index 00000000..6d3c6ee1 --- /dev/null +++ b/src/supermemory/requests/connector_picker.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class ConnectorPickerParams(typing_extensions.TypedDict): + url: str + """ + Send the user here to choose what syncs. Works once, in any browser. + """ + + expires_at: typing_extensions.Annotated[dt.datetime, FieldMetadata(alias="expiresAt")] diff --git a/src/supermemory/requests/connector_selection.py b/src/supermemory/requests/connector_selection.py new file mode 100644 index 00000000..fc197fea --- /dev/null +++ b/src/supermemory/requests/connector_selection.py @@ -0,0 +1,31 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions + + +class ConnectorSelectionParams(typing_extensions.TypedDict): + """ + What to sync, as ids grouped by kind. GitHub takes repos, Gmail takes labels, Google Drive takes files and folders. Replaces the current selection. + """ + + repos: typing_extensions.NotRequired[typing.Sequence[str]] + """ + GitHub repository ids + """ + + labels: typing_extensions.NotRequired[typing.Sequence[str]] + """ + Gmail label ids + """ + + files: typing_extensions.NotRequired[typing.Sequence[str]] + """ + Google Drive file ids + """ + + folders: typing_extensions.NotRequired[typing.Sequence[str]] + """ + Google Drive folder ids + """ diff --git a/src/supermemory/requests/connector_selection_item.py b/src/supermemory/requests/connector_selection_item.py new file mode 100644 index 00000000..d7268fda --- /dev/null +++ b/src/supermemory/requests/connector_selection_item.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + + +class ConnectorSelectionItemParams(typing_extensions.TypedDict): + id: str + name: str diff --git a/src/supermemory/requests/connector_setup.py b/src/supermemory/requests/connector_setup.py new file mode 100644 index 00000000..66c50cf1 --- /dev/null +++ b/src/supermemory/requests/connector_setup.py @@ -0,0 +1,89 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata +from .google_drive_config import GoogleDriveConfigParams +from .granola_config import GranolaConfigParams +from .s3config import S3ConfigParams +from .web_crawler_config import WebCrawlerConfigParams + + +class ConnectorSetup_NotionParams(typing_extensions.TypedDict): + provider: typing.Literal["notion"] + redirect_url: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="redirectUrl")]] + document_limit: typing_extensions.NotRequired[ + typing_extensions.Annotated[int, FieldMetadata(alias="documentLimit")] + ] + + +class ConnectorSetup_GmailParams(typing_extensions.TypedDict): + provider: typing.Literal["gmail"] + redirect_url: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="redirectUrl")]] + document_limit: typing_extensions.NotRequired[ + typing_extensions.Annotated[int, FieldMetadata(alias="documentLimit")] + ] + + +class ConnectorSetup_OnedriveParams(typing_extensions.TypedDict): + provider: typing.Literal["onedrive"] + redirect_url: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="redirectUrl")]] + document_limit: typing_extensions.NotRequired[ + typing_extensions.Annotated[int, FieldMetadata(alias="documentLimit")] + ] + + +class ConnectorSetup_GithubParams(typing_extensions.TypedDict): + provider: typing.Literal["github"] + redirect_url: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="redirectUrl")]] + document_limit: typing_extensions.NotRequired[ + typing_extensions.Annotated[int, FieldMetadata(alias="documentLimit")] + ] + + +class ConnectorSetup_GoogleDriveParams(typing_extensions.TypedDict): + provider: typing.Literal["google-drive"] + redirect_url: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="redirectUrl")]] + document_limit: typing_extensions.NotRequired[ + typing_extensions.Annotated[int, FieldMetadata(alias="documentLimit")] + ] + config: typing_extensions.NotRequired[GoogleDriveConfigParams] + + +class ConnectorSetup_S3Params(typing_extensions.TypedDict): + provider: typing.Literal["s3"] + config: S3ConfigParams + document_limit: typing_extensions.NotRequired[ + typing_extensions.Annotated[int, FieldMetadata(alias="documentLimit")] + ] + + +class ConnectorSetup_WebCrawlerParams(typing_extensions.TypedDict): + provider: typing.Literal["web-crawler"] + config: WebCrawlerConfigParams + document_limit: typing_extensions.NotRequired[ + typing_extensions.Annotated[int, FieldMetadata(alias="documentLimit")] + ] + + +class ConnectorSetup_GranolaParams(typing_extensions.TypedDict): + provider: typing.Literal["granola"] + config: GranolaConfigParams + document_limit: typing_extensions.NotRequired[ + typing_extensions.Annotated[int, FieldMetadata(alias="documentLimit")] + ] + + +ConnectorSetupParams = typing.Union[ + ConnectorSetup_NotionParams, + ConnectorSetup_GmailParams, + ConnectorSetup_OnedriveParams, + ConnectorSetup_GithubParams, + ConnectorSetup_GoogleDriveParams, + ConnectorSetup_S3Params, + ConnectorSetup_WebCrawlerParams, + ConnectorSetup_GranolaParams, +] diff --git a/src/supermemory/requests/connector_setup_github.py b/src/supermemory/requests/connector_setup_github.py new file mode 100644 index 00000000..5935394e --- /dev/null +++ b/src/supermemory/requests/connector_setup_github.py @@ -0,0 +1,18 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class ConnectorSetupGithubParams(typing_extensions.TypedDict): + redirect_url: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="redirectUrl")]] + """ + Where the user returns after authorizing with the provider + """ + + document_limit: typing_extensions.NotRequired[ + typing_extensions.Annotated[int, FieldMetadata(alias="documentLimit")] + ] + """ + Maximum documents this connector imports + """ diff --git a/src/supermemory/requests/connector_setup_gmail.py b/src/supermemory/requests/connector_setup_gmail.py new file mode 100644 index 00000000..09beb069 --- /dev/null +++ b/src/supermemory/requests/connector_setup_gmail.py @@ -0,0 +1,18 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class ConnectorSetupGmailParams(typing_extensions.TypedDict): + redirect_url: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="redirectUrl")]] + """ + Where the user returns after authorizing with the provider + """ + + document_limit: typing_extensions.NotRequired[ + typing_extensions.Annotated[int, FieldMetadata(alias="documentLimit")] + ] + """ + Maximum documents this connector imports + """ diff --git a/src/supermemory/requests/connector_setup_google_drive.py b/src/supermemory/requests/connector_setup_google_drive.py new file mode 100644 index 00000000..554038b2 --- /dev/null +++ b/src/supermemory/requests/connector_setup_google_drive.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from ..core.serialization import FieldMetadata +from .google_drive_config import GoogleDriveConfigParams + + +class ConnectorSetupGoogleDriveParams(typing_extensions.TypedDict): + redirect_url: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="redirectUrl")]] + """ + Where the user returns after authorizing with the provider + """ + + document_limit: typing_extensions.NotRequired[ + typing_extensions.Annotated[int, FieldMetadata(alias="documentLimit")] + ] + """ + Maximum documents this connector imports + """ + + config: typing_extensions.NotRequired[GoogleDriveConfigParams] diff --git a/src/supermemory/requests/connector_setup_granola.py b/src/supermemory/requests/connector_setup_granola.py new file mode 100644 index 00000000..e63f1d6f --- /dev/null +++ b/src/supermemory/requests/connector_setup_granola.py @@ -0,0 +1,19 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from ..core.serialization import FieldMetadata +from .granola_config import GranolaConfigParams + + +class ConnectorSetupGranolaParams(typing_extensions.TypedDict): + config: GranolaConfigParams + """ + Granola API key. Stored encrypted and never returned. + """ + + document_limit: typing_extensions.NotRequired[ + typing_extensions.Annotated[int, FieldMetadata(alias="documentLimit")] + ] + """ + Maximum documents this connector imports + """ diff --git a/src/supermemory/requests/connector_setup_notion.py b/src/supermemory/requests/connector_setup_notion.py new file mode 100644 index 00000000..f284504b --- /dev/null +++ b/src/supermemory/requests/connector_setup_notion.py @@ -0,0 +1,18 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class ConnectorSetupNotionParams(typing_extensions.TypedDict): + redirect_url: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="redirectUrl")]] + """ + Where the user returns after authorizing with the provider + """ + + document_limit: typing_extensions.NotRequired[ + typing_extensions.Annotated[int, FieldMetadata(alias="documentLimit")] + ] + """ + Maximum documents this connector imports + """ diff --git a/src/supermemory/requests/connector_setup_onedrive.py b/src/supermemory/requests/connector_setup_onedrive.py new file mode 100644 index 00000000..05e7e27c --- /dev/null +++ b/src/supermemory/requests/connector_setup_onedrive.py @@ -0,0 +1,18 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class ConnectorSetupOnedriveParams(typing_extensions.TypedDict): + redirect_url: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="redirectUrl")]] + """ + Where the user returns after authorizing with the provider + """ + + document_limit: typing_extensions.NotRequired[ + typing_extensions.Annotated[int, FieldMetadata(alias="documentLimit")] + ] + """ + Maximum documents this connector imports + """ diff --git a/src/supermemory/requests/connector_setup_result.py b/src/supermemory/requests/connector_setup_result.py new file mode 100644 index 00000000..47a09b67 --- /dev/null +++ b/src/supermemory/requests/connector_setup_result.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class ConnectorSetupResultParams(typing_extensions.TypedDict): + id: str + """ + Connector identifier + """ + + auth_url: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="authUrl")] + """ + Send the user here to authorize the provider. Null for providers that authenticate with config only. + """ + + auth_url_expires_at: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="authUrlExpiresAt") + ] + """ + When authUrl stops working + """ diff --git a/src/supermemory/requests/connector_setup_s3.py b/src/supermemory/requests/connector_setup_s3.py new file mode 100644 index 00000000..0a305071 --- /dev/null +++ b/src/supermemory/requests/connector_setup_s3.py @@ -0,0 +1,19 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from ..core.serialization import FieldMetadata +from .s3config import S3ConfigParams + + +class ConnectorSetupS3Params(typing_extensions.TypedDict): + config: S3ConfigParams + """ + S3 bucket to sync. Credentials are stored encrypted and never returned. + """ + + document_limit: typing_extensions.NotRequired[ + typing_extensions.Annotated[int, FieldMetadata(alias="documentLimit")] + ] + """ + Maximum documents this connector imports + """ diff --git a/src/supermemory/requests/connector_setup_web_crawler.py b/src/supermemory/requests/connector_setup_web_crawler.py new file mode 100644 index 00000000..a4855932 --- /dev/null +++ b/src/supermemory/requests/connector_setup_web_crawler.py @@ -0,0 +1,19 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from ..core.serialization import FieldMetadata +from .web_crawler_config import WebCrawlerConfigParams + + +class ConnectorSetupWebCrawlerParams(typing_extensions.TypedDict): + config: WebCrawlerConfigParams + """ + Website to crawl + """ + + document_limit: typing_extensions.NotRequired[ + typing_extensions.Annotated[int, FieldMetadata(alias="documentLimit")] + ] + """ + Maximum documents this connector imports + """ diff --git a/src/supermemory/requests/connector_sync.py b/src/supermemory/requests/connector_sync.py new file mode 100644 index 00000000..e9c52ab2 --- /dev/null +++ b/src/supermemory/requests/connector_sync.py @@ -0,0 +1,31 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata +from ..types.connector_error_code import ConnectorErrorCode +from ..types.connector_sync_status import ConnectorSyncStatus +from ..types.connector_sync_trigger import ConnectorSyncTrigger +from .connector_sync_failure import ConnectorSyncFailureParams + + +class ConnectorSyncParams(typing_extensions.TypedDict): + status: ConnectorSyncStatus + error_code: typing_extensions.Annotated[typing.Optional[ConnectorErrorCode], FieldMetadata(alias="errorCode")] + error: typing.Optional[str] + """ + Customer-safe error message + """ + + started_at: typing_extensions.Annotated[dt.datetime, FieldMetadata(alias="startedAt")] + completed_at: typing_extensions.Annotated[typing.Optional[dt.datetime], FieldMetadata(alias="completedAt")] + id: str + trigger: ConnectorSyncTrigger + items_processed: typing_extensions.Annotated[float, FieldMetadata(alias="itemsProcessed")] + items_failed: typing_extensions.Annotated[float, FieldMetadata(alias="itemsFailed")] + failures: typing.Sequence[ConnectorSyncFailureParams] + """ + Failed items, capped. itemsFailed is the true count. + """ diff --git a/src/supermemory/requests/connector_sync_failure.py b/src/supermemory/requests/connector_sync_failure.py new file mode 100644 index 00000000..7e135b2b --- /dev/null +++ b/src/supermemory/requests/connector_sync_failure.py @@ -0,0 +1,14 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class ConnectorSyncFailureParams(typing_extensions.TypedDict): + document_id: typing_extensions.Annotated[str, FieldMetadata(alias="documentId")] + title: typing.Optional[str] + error_code: typing_extensions.Annotated[str, FieldMetadata(alias="errorCode")] + error_message: typing_extensions.Annotated[str, FieldMetadata(alias="errorMessage")] + failed_at: typing_extensions.Annotated[str, FieldMetadata(alias="failedAt")] diff --git a/src/supermemory/requests/connector_sync_started.py b/src/supermemory/requests/connector_sync_started.py new file mode 100644 index 00000000..a1b5c6b8 --- /dev/null +++ b/src/supermemory/requests/connector_sync_started.py @@ -0,0 +1,14 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions + + +class ConnectorSyncStartedParams(typing_extensions.TypedDict): + id: str + """ + Connector identifier + """ + + status: typing.Literal["queued"] diff --git a/src/supermemory/requests/delete_document_error.py b/src/supermemory/requests/delete_document_error.py new file mode 100644 index 00000000..a5001326 --- /dev/null +++ b/src/supermemory/requests/delete_document_error.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + + +class DeleteDocumentErrorParams(typing_extensions.TypedDict): + id: str + """ + The requested document identifier + """ + + error: str + """ + Why this document could not be deleted + """ diff --git a/src/supermemory/requests/delete_documents_result.py b/src/supermemory/requests/delete_documents_result.py new file mode 100644 index 00000000..240f563b --- /dev/null +++ b/src/supermemory/requests/delete_documents_result.py @@ -0,0 +1,19 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata +from .delete_document_error import DeleteDocumentErrorParams + + +class DeleteDocumentsResultParams(typing_extensions.TypedDict): + deleted_count: typing_extensions.Annotated[int, FieldMetadata(alias="deletedCount")] + """ + Number of documents successfully deleted + """ + + errors: typing.Sequence[DeleteDocumentErrorParams] + """ + Per-document failures; successful deletions are not rolled back + """ diff --git a/src/supermemory/requests/document.py b/src/supermemory/requests/document.py new file mode 100644 index 00000000..31a08750 --- /dev/null +++ b/src/supermemory/requests/document.py @@ -0,0 +1,59 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from .chunk import ChunkParams +from .document_system import DocumentSystemParams +from .memory import MemoryParams + + +class DocumentParams(typing_extensions.TypedDict): + """ + Namespace-scoped document. Attachment keys are omitted unless requested and are empty arrays when requested without results. + """ + + id: str + """ + Stable document identifier + """ + + title: typing.Optional[str] + """ + Extracted or caller-provided document title + """ + + type: str + """ + Detected source or content type + """ + + summary: typing.Optional[str] + """ + Generated summary of the document, when available + """ + + content: typing.Optional[str] + """ + Canonical extracted text used for processing and recall + """ + + metadata: typing.Dict[str, typing.Any] + """ + Caller metadata with internal fields removed + """ + + system: DocumentSystemParams + """ + Processing status, lifecycle timestamps, and storage path maintained by Supermemory + """ + + chunks: typing_extensions.NotRequired[typing.Sequence[ChunkParams]] + """ + Ordered document chunks; included only when requested + """ + + memories: typing_extensions.NotRequired[typing.Sequence[MemoryParams]] + """ + Memories derived from the document; included only when requested + """ diff --git a/src/supermemory/requests/document_input.py b/src/supermemory/requests/document_input.py new file mode 100644 index 00000000..9f9c1b26 --- /dev/null +++ b/src/supermemory/requests/document_input.py @@ -0,0 +1,41 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata +from .metadata_value import MetadataValueParams + + +class DocumentInputParams(typing_extensions.TypedDict): + content: str + """ + The content to process. This may be plaintext or a URL to supported rich content. + """ + + id: typing_extensions.NotRequired[str] + """ + An optional caller-defined document ID + """ + + supporting_context: typing_extensions.NotRequired[ + typing_extensions.Annotated[str, FieldMetadata(alias="supportingContext")] + ] + """ + Context used to guide memory extraction within this namespace. Max 1500 characters. + """ + + metadata: typing_extensions.NotRequired[typing.Dict[str, MetadataValueParams]] + """ + Arbitrary metadata attached to the document and derived data + """ + + group: typing_extensions.NotRequired[typing.Dict[str, MetadataValueParams]] + """ + Metadata values that isolate related context within the namespace + """ + + date: typing_extensions.NotRequired[str] + """ + When the source content is from, in ISO 8601 format + """ diff --git a/src/supermemory/requests/document_list_item.py b/src/supermemory/requests/document_list_item.py new file mode 100644 index 00000000..5fa297cb --- /dev/null +++ b/src/supermemory/requests/document_list_item.py @@ -0,0 +1,47 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from .document_system import DocumentSystemParams + + +class DocumentListItemParams(typing_extensions.TypedDict): + """ + A document in this namespace without its content + """ + + id: str + """ + Stable document identifier + """ + + title: typing.Optional[str] + """ + Extracted or caller-provided document title + """ + + type: str + """ + Detected source or content type + """ + + summary: typing.Optional[str] + """ + Generated summary of the document, when available + """ + + metadata: typing.Dict[str, typing.Any] + """ + Public metadata attached to the document + """ + + url: typing.Optional[str] + """ + Original source URL, when the document was ingested from the web + """ + + system: DocumentSystemParams + """ + Processing status, lifecycle timestamps, and storage path maintained by Supermemory + """ diff --git a/src/supermemory/requests/document_ref.py b/src/supermemory/requests/document_ref.py new file mode 100644 index 00000000..a5319b65 --- /dev/null +++ b/src/supermemory/requests/document_ref.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + + +class DocumentRefParams(typing_extensions.TypedDict): + id: str + """ + Unique identifier of the document + """ + + status: str + """ + Status of the document + """ diff --git a/src/supermemory/requests/document_system.py b/src/supermemory/requests/document_system.py new file mode 100644 index 00000000..a36cef33 --- /dev/null +++ b/src/supermemory/requests/document_system.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class DocumentSystemParams(typing_extensions.TypedDict): + """ + Processing status, lifecycle timestamps, and storage path maintained by Supermemory + """ + + status: str + """ + Current extraction and memory-processing status + """ + + created_at: typing_extensions.Annotated[dt.datetime, FieldMetadata(alias="createdAt")] + """ + ISO 8601 timestamp when the document was created + """ + + updated_at: typing_extensions.Annotated[dt.datetime, FieldMetadata(alias="updatedAt")] + """ + ISO 8601 timestamp of the latest document update + """ diff --git a/src/supermemory/requests/error_response.py b/src/supermemory/requests/error_response.py new file mode 100644 index 00000000..2ecf7a59 --- /dev/null +++ b/src/supermemory/requests/error_response.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + + +class ErrorResponseParams(typing_extensions.TypedDict): + error: str + """ + Error message + """ + + details: typing_extensions.NotRequired[str] + """ + Additional error details + """ diff --git a/src/supermemory/requests/file_upload_result.py b/src/supermemory/requests/file_upload_result.py new file mode 100644 index 00000000..4ecbd43d --- /dev/null +++ b/src/supermemory/requests/file_upload_result.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions + + +class FileUploadResultParams(typing_extensions.TypedDict): + id: str + """ + Document identifier to poll or retrieve + """ + + status: typing.Literal["processing"] + """ + Confirms the file was accepted for asynchronous processing + """ diff --git a/src/supermemory/requests/filter_and.py b/src/supermemory/requests/filter_and.py new file mode 100644 index 00000000..d80013bd --- /dev/null +++ b/src/supermemory/requests/filter_and.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +import typing_extensions + +if typing.TYPE_CHECKING: + from .filter_expression import FilterExpressionParams + + +class FilterAndParams(typing_extensions.TypedDict): + operator: typing.Literal["and"] + """ + Require every nested filter expression to match + """ + + operands: typing.Sequence["FilterExpressionParams"] + """ + Filter expressions combined with logical AND + """ diff --git a/src/supermemory/requests/filter_array_contains.py b/src/supermemory/requests/filter_array_contains.py new file mode 100644 index 00000000..43d19f9c --- /dev/null +++ b/src/supermemory/requests/filter_array_contains.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from ..types.filter_array_contains_operator import FilterArrayContainsOperator + + +class FilterArrayContainsParams(typing_extensions.TypedDict): + field: str + """ + Metadata key to evaluate, matched literally. A period is part of the key name, not a path into nested metadata. + """ + + operator: FilterArrayContainsOperator + """ + Require or exclude an exact array member + """ + + value: str + """ + Array member to look for + """ diff --git a/src/supermemory/requests/filter_expression.py b/src/supermemory/requests/filter_expression.py new file mode 100644 index 00000000..a0038a4d --- /dev/null +++ b/src/supermemory/requests/filter_expression.py @@ -0,0 +1,12 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +from .filter_predicate import FilterPredicateParams + +if typing.TYPE_CHECKING: + from .filter_and import FilterAndParams + from .filter_or import FilterOrParams +FilterExpressionParams = typing.Union[FilterPredicateParams, "FilterAndParams", "FilterOrParams"] diff --git a/src/supermemory/requests/filter_number_compare.py b/src/supermemory/requests/filter_number_compare.py new file mode 100644 index 00000000..452d8921 --- /dev/null +++ b/src/supermemory/requests/filter_number_compare.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from ..types.filter_number_compare_operator import FilterNumberCompareOperator + + +class FilterNumberCompareParams(typing_extensions.TypedDict): + field: str + """ + Metadata key to evaluate, matched literally. A period is part of the key name, not a path into nested metadata. + """ + + operator: FilterNumberCompareOperator + """ + Numeric comparison to apply + """ + + value: float + """ + Numeric value to compare against + """ diff --git a/src/supermemory/requests/filter_or.py b/src/supermemory/requests/filter_or.py new file mode 100644 index 00000000..2fba5e56 --- /dev/null +++ b/src/supermemory/requests/filter_or.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +import typing_extensions + +if typing.TYPE_CHECKING: + from .filter_expression import FilterExpressionParams + + +class FilterOrParams(typing_extensions.TypedDict): + operator: typing.Literal["or"] + """ + Require at least one nested filter expression to match + """ + + operands: typing.Sequence["FilterExpressionParams"] + """ + Filter expressions combined with logical OR + """ diff --git a/src/supermemory/requests/filter_predicate.py b/src/supermemory/requests/filter_predicate.py new file mode 100644 index 00000000..4d91bdf8 --- /dev/null +++ b/src/supermemory/requests/filter_predicate.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .filter_array_contains import FilterArrayContainsParams +from .filter_number_compare import FilterNumberCompareParams +from .filter_string_contains import FilterStringContainsParams +from .filter_string_equals import FilterStringEqualsParams +from .filter_value_equals import FilterValueEqualsParams + +FilterPredicateParams = typing.Union[ + FilterStringEqualsParams, + FilterValueEqualsParams, + FilterNumberCompareParams, + FilterStringContainsParams, + FilterArrayContainsParams, +] diff --git a/src/supermemory/requests/filter_string_contains.py b/src/supermemory/requests/filter_string_contains.py new file mode 100644 index 00000000..6a96f6d3 --- /dev/null +++ b/src/supermemory/requests/filter_string_contains.py @@ -0,0 +1,29 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from ..core.serialization import FieldMetadata +from ..types.filter_string_contains_operator import FilterStringContainsOperator + + +class FilterStringContainsParams(typing_extensions.TypedDict): + field: str + """ + Metadata key to evaluate, matched literally. A period is part of the key name, not a path into nested metadata. + """ + + operator: FilterStringContainsOperator + """ + Require or exclude a substring match + """ + + value: str + """ + Substring to look for + """ + + case_sensitive: typing_extensions.NotRequired[ + typing_extensions.Annotated[bool, FieldMetadata(alias="caseSensitive")] + ] + """ + Whether substring matching preserves letter case + """ diff --git a/src/supermemory/requests/filter_string_equals.py b/src/supermemory/requests/filter_string_equals.py new file mode 100644 index 00000000..21f8442f --- /dev/null +++ b/src/supermemory/requests/filter_string_equals.py @@ -0,0 +1,29 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from ..core.serialization import FieldMetadata +from ..types.filter_string_equals_operator import FilterStringEqualsOperator + + +class FilterStringEqualsParams(typing_extensions.TypedDict): + field: str + """ + Metadata key to evaluate, matched literally. A period is part of the key name, not a path into nested metadata. + """ + + operator: FilterStringEqualsOperator + """ + Compare the field for equality or inequality + """ + + value: str + """ + String value to compare against + """ + + case_sensitive: typing_extensions.NotRequired[ + typing_extensions.Annotated[bool, FieldMetadata(alias="caseSensitive")] + ] + """ + Whether string comparison preserves letter case + """ diff --git a/src/supermemory/requests/filter_value_equals.py b/src/supermemory/requests/filter_value_equals.py new file mode 100644 index 00000000..690861a1 --- /dev/null +++ b/src/supermemory/requests/filter_value_equals.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from ..types.filter_value_equals_operator import FilterValueEqualsOperator +from .filter_value_equals_value import FilterValueEqualsValueParams + + +class FilterValueEqualsParams(typing_extensions.TypedDict): + field: str + """ + Metadata key to evaluate, matched literally. A period is part of the key name, not a path into nested metadata. + """ + + operator: FilterValueEqualsOperator + """ + Compare the field for equality or inequality + """ + + value: FilterValueEqualsValueParams + """ + Numeric or boolean value to compare against + """ diff --git a/src/supermemory/requests/filter_value_equals_value.py b/src/supermemory/requests/filter_value_equals_value.py new file mode 100644 index 00000000..c0bba9f6 --- /dev/null +++ b/src/supermemory/requests/filter_value_equals_value.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +FilterValueEqualsValueParams = typing.Union[float, bool] diff --git a/src/supermemory/requests/forget_error.py b/src/supermemory/requests/forget_error.py new file mode 100644 index 00000000..fbdfb72c --- /dev/null +++ b/src/supermemory/requests/forget_error.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + + +class ForgetErrorParams(typing_extensions.TypedDict): + id: str + """ + Requested memory identifier + """ + + error: str + """ + Why the memory could not be forgotten + """ diff --git a/src/supermemory/requests/forget_match.py b/src/supermemory/requests/forget_match.py new file mode 100644 index 00000000..778408a4 --- /dev/null +++ b/src/supermemory/requests/forget_match.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + + +class ForgetMatchParams(typing_extensions.TypedDict): + id: str + """ + Stable identifier of the matched memory + """ + + memory: str + """ + Matched memory text + """ diff --git a/src/supermemory/requests/forget_result.py b/src/supermemory/requests/forget_result.py new file mode 100644 index 00000000..ee21b0cb --- /dev/null +++ b/src/supermemory/requests/forget_result.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from .forget_error import ForgetErrorParams +from .forget_match import ForgetMatchParams + + +class ForgetResultParams(typing_extensions.TypedDict): + count: int + """ + Number of memories matched by this operation + """ + + errors: typing.Sequence[ForgetErrorParams] + """ + Per-memory failures for exact-ID requests + """ + + matches: typing.Sequence[ForgetMatchParams] + """ + Memories selected by the operation, whether previewed or forgotten + """ diff --git a/src/supermemory/requests/google_drive_config.py b/src/supermemory/requests/google_drive_config.py new file mode 100644 index 00000000..2750e398 --- /dev/null +++ b/src/supermemory/requests/google_drive_config.py @@ -0,0 +1,14 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from ..core.serialization import FieldMetadata +from ..types.google_drive_config_sync_scope import GoogleDriveConfigSyncScope + + +class GoogleDriveConfigParams(typing_extensions.TypedDict): + sync_scope: typing_extensions.NotRequired[ + typing_extensions.Annotated[GoogleDriveConfigSyncScope, FieldMetadata(alias="syncScope")] + ] + """ + selected (default) syncs only files the user picks. full syncs the whole Drive. + """ diff --git a/src/supermemory/requests/granola_config.py b/src/supermemory/requests/granola_config.py new file mode 100644 index 00000000..1579b9fb --- /dev/null +++ b/src/supermemory/requests/granola_config.py @@ -0,0 +1,12 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class GranolaConfigParams(typing_extensions.TypedDict): + """ + Granola API key. Stored encrypted and never returned. + """ + + api_key: typing_extensions.Annotated[str, FieldMetadata(alias="apiKey")] diff --git a/src/supermemory/requests/list_request_limit.py b/src/supermemory/requests/list_request_limit.py new file mode 100644 index 00000000..7b70cac2 --- /dev/null +++ b/src/supermemory/requests/list_request_limit.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +ListRequestLimitParams = typing.Union[str, float] diff --git a/src/supermemory/requests/list_request_page.py b/src/supermemory/requests/list_request_page.py new file mode 100644 index 00000000..84cfacd9 --- /dev/null +++ b/src/supermemory/requests/list_request_page.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +ListRequestPageParams = typing.Union[str, float] diff --git a/src/supermemory/requests/list_response.py b/src/supermemory/requests/list_response.py new file mode 100644 index 00000000..d6579804 --- /dev/null +++ b/src/supermemory/requests/list_response.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from .chunk_list_item import ChunkListItemParams +from .document_list_item import DocumentListItemParams +from .memory import MemoryParams +from .pagination import PaginationParams + + +class ListResponseParams(typing_extensions.TypedDict): + """ + A namespace-scoped page of one resource type + """ + + documents: typing.Sequence[DocumentListItemParams] + """ + Documents on this page; empty when a different resource type is requested + """ + + chunks: typing.Sequence[ChunkListItemParams] + """ + Chunks on this page; empty when a different resource type is requested + """ + + memories: typing.Sequence[MemoryParams] + """ + Memories on this page; empty when a different resource type is requested + """ + + pagination: PaginationParams + """ + Page metadata for the selected resource collection + """ diff --git a/src/supermemory/requests/memory.py b/src/supermemory/requests/memory.py new file mode 100644 index 00000000..2b4eddfb --- /dev/null +++ b/src/supermemory/requests/memory.py @@ -0,0 +1,58 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata +from .memory_system import MemorySystemParams + + +class MemoryParams(typing_extensions.TypedDict): + """ + A memory formed from documents in this namespace + """ + + id: str + """ + Stable memory identifier + """ + + memory: str + """ + Learned fact or context extracted from the document + """ + + metadata: typing.Optional[typing.Dict[str, typing.Any]] + """ + Memory metadata, including temporal context when available + """ + + is_static: typing_extensions.Annotated[bool, FieldMetadata(alias="isStatic")] + """ + Whether the memory belongs to the stable profile section + """ + + is_inference: typing_extensions.Annotated[bool, FieldMetadata(alias="isInference")] + """ + Whether the memory was inferred rather than explicitly stated + """ + + is_latest: typing_extensions.Annotated[bool, FieldMetadata(alias="isLatest")] + """ + Whether this is the newest version in its memory chain + """ + + is_forgotten: typing_extensions.Annotated[bool, FieldMetadata(alias="isForgotten")] + """ + Whether this memory has been excluded from normal recall + """ + + version: int + """ + Version number within the memory chain + """ + + system: MemorySystemParams + """ + Lifecycle timestamps maintained by Supermemory + """ diff --git a/src/supermemory/requests/memory_system.py b/src/supermemory/requests/memory_system.py new file mode 100644 index 00000000..879202da --- /dev/null +++ b/src/supermemory/requests/memory_system.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class MemorySystemParams(typing_extensions.TypedDict): + """ + Lifecycle timestamps maintained by Supermemory + """ + + created_at: typing_extensions.Annotated[dt.datetime, FieldMetadata(alias="createdAt")] + """ + ISO 8601 timestamp when this memory version was created + """ + + updated_at: typing_extensions.Annotated[dt.datetime, FieldMetadata(alias="updatedAt")] + """ + ISO 8601 timestamp of the latest update to this version + """ diff --git a/src/supermemory/requests/metadata_value.py b/src/supermemory/requests/metadata_value.py new file mode 100644 index 00000000..9b1e4cd3 --- /dev/null +++ b/src/supermemory/requests/metadata_value.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +MetadataValueParams = typing.Union[str, float, bool, typing.Sequence[str]] diff --git a/src/supermemory/requests/namespace.py b/src/supermemory/requests/namespace.py new file mode 100644 index 00000000..03949b1b --- /dev/null +++ b/src/supermemory/requests/namespace.py @@ -0,0 +1,39 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata +from .namespace_system import NamespaceSystemParams + + +class NamespaceParams(typing_extensions.TypedDict): + id: str + """ + Internal identifier for the namespace + """ + + namespace: str + """ + Namespace identifier used in API paths + """ + + document_count: typing_extensions.Annotated[int, FieldMetadata(alias="documentCount")] + """ + Number of active source documents in the namespace + """ + + memory_count: typing_extensions.Annotated[int, FieldMetadata(alias="memoryCount")] + """ + Number of current, recallable memories in the namespace + """ + + description: typing.Optional[str] + """ + Human-readable purpose or scope of the namespace + """ + + system: NamespaceSystemParams + """ + Namespace lifecycle timestamps + """ diff --git a/src/supermemory/requests/namespace_deleted.py b/src/supermemory/requests/namespace_deleted.py new file mode 100644 index 00000000..721d3951 --- /dev/null +++ b/src/supermemory/requests/namespace_deleted.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class NamespaceDeletedParams(typing_extensions.TypedDict): + success: typing.Literal[True] + """ + Confirms the namespace was deleted + """ + + namespace: str + """ + Deleted namespace identifier + """ + + deleted_documents_count: typing_extensions.Annotated[int, FieldMetadata(alias="deletedDocumentsCount")] + """ + Number of documents permanently removed + """ + + deleted_memories_count: typing_extensions.Annotated[int, FieldMetadata(alias="deletedMemoriesCount")] + """ + Number of memories permanently removed + """ diff --git a/src/supermemory/requests/namespace_details.py b/src/supermemory/requests/namespace_details.py new file mode 100644 index 00000000..884ce99d --- /dev/null +++ b/src/supermemory/requests/namespace_details.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata +from .namespace_system import NamespaceSystemParams + + +class NamespaceDetailsParams(typing_extensions.TypedDict): + namespace: str + """ + Namespace identifier used in API paths + """ + + supporting_context: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="supportingContext")] + """ + Background that helps Supermemory interpret documents and form better memories in this namespace + """ + + system: NamespaceSystemParams + """ + Namespace lifecycle timestamps + """ diff --git a/src/supermemory/requests/namespace_system.py b/src/supermemory/requests/namespace_system.py new file mode 100644 index 00000000..4c11109e --- /dev/null +++ b/src/supermemory/requests/namespace_system.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class NamespaceSystemParams(typing_extensions.TypedDict): + """ + Namespace lifecycle timestamps + """ + + created_at: typing_extensions.Annotated[dt.datetime, FieldMetadata(alias="createdAt")] + """ + ISO 8601 timestamp when the namespace was created + """ + + updated_at: typing_extensions.Annotated[dt.datetime, FieldMetadata(alias="updatedAt")] + """ + ISO 8601 timestamp of the latest namespace update + """ diff --git a/src/supermemory/requests/organization.py b/src/supermemory/requests/organization.py new file mode 100644 index 00000000..8d4844c7 --- /dev/null +++ b/src/supermemory/requests/organization.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class OrganizationParams(typing_extensions.TypedDict): + organizational_context: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="organizationalContext") + ] + """ + Shared background that helps Supermemory interpret content consistently across every namespace + """ + + namespace_count: typing_extensions.Annotated[int, FieldMetadata(alias="namespaceCount")] + """ + Number of namespaces in the organization + """ diff --git a/src/supermemory/requests/pagination.py b/src/supermemory/requests/pagination.py new file mode 100644 index 00000000..4707336a --- /dev/null +++ b/src/supermemory/requests/pagination.py @@ -0,0 +1,30 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class PaginationParams(typing_extensions.TypedDict): + """ + Page metadata for the selected resource collection + """ + + current_page: typing_extensions.Annotated[float, FieldMetadata(alias="currentPage")] + """ + Current one-based page number + """ + + limit: typing_extensions.NotRequired[float] + """ + Maximum resources returned per page + """ + + total_items: typing_extensions.Annotated[float, FieldMetadata(alias="totalItems")] + """ + Total resources matching the request + """ + + total_pages: typing_extensions.Annotated[float, FieldMetadata(alias="totalPages")] + """ + Total pages available at the selected limit + """ diff --git a/src/supermemory/requests/profile.py b/src/supermemory/requests/profile.py new file mode 100644 index 00000000..601bf109 --- /dev/null +++ b/src/supermemory/requests/profile.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from .profile_fact import ProfileFactParams + + +class ProfileParams(typing_extensions.TypedDict): + """ + Continuously maintained understanding of the subject represented by this namespace + """ + + static: typing.Sequence[ProfileFactParams] + """ + Durable facts that define the subject and rarely change, such as identity or long-term preferences + """ + + dynamic: typing.Sequence[ProfileFactParams] + """ + Recent or evolving context, such as current goals, projects, and active interests + """ + + buckets: typing.Dict[str, typing.Sequence[ProfileFactParams]] + """ + Custom profile sections keyed by bucket name, with the memories currently assigned to each bucket + """ diff --git a/src/supermemory/requests/profile_buckets.py b/src/supermemory/requests/profile_buckets.py new file mode 100644 index 00000000..0a1f29f4 --- /dev/null +++ b/src/supermemory/requests/profile_buckets.py @@ -0,0 +1,12 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions + + +class ProfileBucketsParams(typing_extensions.TypedDict): + buckets: typing.Dict[str, str] + """ + Effective bucket definitions keyed by name. Includes organization buckets and namespace-owned additions. + """ diff --git a/src/supermemory/requests/profile_fact.py b/src/supermemory/requests/profile_fact.py new file mode 100644 index 00000000..fb8658e6 --- /dev/null +++ b/src/supermemory/requests/profile_fact.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + + +class ProfileFactParams(typing_extensions.TypedDict): + id: str + """ + Memory ID, or an aggregated_-prefixed ID for a summary synthesized from several memories + """ + + memory: str + """ + Memory text + """ diff --git a/src/supermemory/requests/profile_response.py b/src/supermemory/requests/profile_response.py new file mode 100644 index 00000000..176fd394 --- /dev/null +++ b/src/supermemory/requests/profile_response.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from .profile import ProfileParams + + +class ProfileResponseParams(typing_extensions.TypedDict): + profile: ProfileParams + """ + Continuously maintained understanding of the subject represented by this namespace + """ diff --git a/src/supermemory/requests/related_memories.py b/src/supermemory/requests/related_memories.py new file mode 100644 index 00000000..b65de042 --- /dev/null +++ b/src/supermemory/requests/related_memories.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from .related_memory import RelatedMemoryParams + + +class RelatedMemoriesParams(typing_extensions.TypedDict): + """ + Memory relationships attached when requested + """ + + parents: typing.Sequence[RelatedMemoryParams] + """ + Earlier memories this result updates or derives from + """ + + children: typing.Sequence[RelatedMemoryParams] + """ + Newer memories that update or extend this result + """ + + siblings: typing.Sequence[RelatedMemoryParams] + """ + Other memories derived from related context + """ diff --git a/src/supermemory/requests/related_memory.py b/src/supermemory/requests/related_memory.py new file mode 100644 index 00000000..618b647d --- /dev/null +++ b/src/supermemory/requests/related_memory.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..types.memory_relation import MemoryRelation +from .related_memory_system import RelatedMemorySystemParams + + +class RelatedMemoryParams(typing_extensions.TypedDict): + relation: MemoryRelation + """ + How this memory is connected to the matched memory + """ + + version: typing_extensions.NotRequired[typing.Optional[float]] + """ + Version number within the related memory's history + """ + + memory: str + """ + Related learned fact or context + """ + + metadata: typing_extensions.NotRequired[typing.Optional[typing.Dict[str, typing.Any]]] + """ + Public metadata associated with the related memory + """ + + system: RelatedMemorySystemParams + """ + Lifecycle details for the related memory + """ diff --git a/src/supermemory/requests/related_memory_system.py b/src/supermemory/requests/related_memory_system.py new file mode 100644 index 00000000..6bf40683 --- /dev/null +++ b/src/supermemory/requests/related_memory_system.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class RelatedMemorySystemParams(typing_extensions.TypedDict): + """ + Lifecycle details for the related memory + """ + + updated_at: typing_extensions.Annotated[str, FieldMetadata(alias="updatedAt")] + """ + ISO 8601 timestamp of the related memory's latest update + """ diff --git a/src/supermemory/requests/s3config.py b/src/supermemory/requests/s3config.py new file mode 100644 index 00000000..10998bf4 --- /dev/null +++ b/src/supermemory/requests/s3config.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class S3ConfigParams(typing_extensions.TypedDict): + """ + S3 bucket to sync. Credentials are stored encrypted and never returned. + """ + + bucket: str + region: str + access_key_id: typing_extensions.Annotated[str, FieldMetadata(alias="accessKeyId")] + secret_access_key: typing_extensions.Annotated[str, FieldMetadata(alias="secretAccessKey")] + endpoint: typing_extensions.NotRequired[str] + """ + For S3-compatible stores such as R2 or MinIO + """ + + prefix: typing_extensions.NotRequired[str] + """ + Only sync keys under this prefix + """ diff --git a/src/supermemory/requests/search_request_attach.py b/src/supermemory/requests/search_request_attach.py new file mode 100644 index 00000000..9f17bef3 --- /dev/null +++ b/src/supermemory/requests/search_request_attach.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + + +class SearchRequestAttachParams(typing_extensions.TypedDict): + """ + Optional context to include alongside each matching result + """ + + documents: typing_extensions.NotRequired[bool] + """ + Attach the source document for each result when one is available + """ + + related: typing_extensions.NotRequired[bool] + """ + Attach parent, child, and sibling memories that explain how each memory evolved + """ + + forgotten: typing_extensions.NotRequired[bool] + """ + Allow forgotten memories to participate in recall + """ diff --git a/src/supermemory/requests/search_response.py b/src/supermemory/requests/search_response.py new file mode 100644 index 00000000..a70dc717 --- /dev/null +++ b/src/supermemory/requests/search_response.py @@ -0,0 +1,19 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata +from .search_result import SearchResultParams + + +class SearchResponseParams(typing_extensions.TypedDict): + results: typing.Sequence[SearchResultParams] + """ + Ranked memories and source chunks matching the query + """ + + search_time: typing_extensions.Annotated[float, FieldMetadata(alias="searchTime")] + """ + Server-side search duration in milliseconds + """ diff --git a/src/supermemory/requests/search_result.py b/src/supermemory/requests/search_result.py new file mode 100644 index 00000000..9b0694b0 --- /dev/null +++ b/src/supermemory/requests/search_result.py @@ -0,0 +1,50 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata +from .search_result_included import SearchResultIncludedParams +from .search_result_system import SearchResultSystemParams + + +class SearchResultParams(typing_extensions.TypedDict): + id: str + """ + Stable identifier for the matching result + """ + + memory: typing_extensions.NotRequired[str] + """ + Learned fact or context returned by memory search (only present for memory results) + """ + + chunk: typing_extensions.NotRequired[str] + """ + Source passage returned by chunk search (only present for chunk results from hybrid search) + """ + + metadata: typing.Optional[typing.Dict[str, typing.Any]] + """ + Public metadata attached to the result + """ + + similarity: float + """ + Normalized relevance score used to rank the result, from 0 to 1 + """ + + is_latest: typing_extensions.Annotated[bool, FieldMetadata(alias="isLatest")] + """ + Whether the memory is its latest version; false for recalled forgotten memories + """ + + system: SearchResultSystemParams + """ + Lifecycle and filesystem details for the result + """ + + included: typing_extensions.NotRequired[SearchResultIncludedParams] + """ + Requested supporting context for the result + """ diff --git a/src/supermemory/requests/search_result_included.py b/src/supermemory/requests/search_result_included.py new file mode 100644 index 00000000..1eac276c --- /dev/null +++ b/src/supermemory/requests/search_result_included.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from .related_memories import RelatedMemoriesParams +from .search_result_included_document import SearchResultIncludedDocumentParams + + +class SearchResultIncludedParams(typing_extensions.TypedDict): + """ + Requested supporting context for the result + """ + + related: typing_extensions.NotRequired[RelatedMemoriesParams] + """ + Memory relationships attached when requested + """ + + document: typing_extensions.NotRequired[SearchResultIncludedDocumentParams] + """ + Source document attached when requested and available + """ diff --git a/src/supermemory/requests/search_result_included_document.py b/src/supermemory/requests/search_result_included_document.py new file mode 100644 index 00000000..efd876d9 --- /dev/null +++ b/src/supermemory/requests/search_result_included_document.py @@ -0,0 +1,47 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class SearchResultIncludedDocumentParams(typing_extensions.TypedDict): + """ + Source document attached when requested and available + """ + + id: str + """ + Source document identifier + """ + + title: typing_extensions.NotRequired[typing.Optional[str]] + """ + Source document title + """ + + type: typing_extensions.NotRequired[typing.Optional[str]] + """ + Detected source or content type + """ + + metadata: typing_extensions.NotRequired[typing.Optional[typing.Dict[str, typing.Any]]] + """ + Public metadata attached to the source document + """ + + summary: typing_extensions.NotRequired[typing.Optional[str]] + """ + Generated summary of the source document + """ + + created_at: typing_extensions.Annotated[str, FieldMetadata(alias="createdAt")] + """ + ISO 8601 timestamp when the source document was created + """ + + updated_at: typing_extensions.Annotated[str, FieldMetadata(alias="updatedAt")] + """ + ISO 8601 timestamp of the source document's latest update + """ diff --git a/src/supermemory/requests/search_result_system.py b/src/supermemory/requests/search_result_system.py new file mode 100644 index 00000000..76d518a6 --- /dev/null +++ b/src/supermemory/requests/search_result_system.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class SearchResultSystemParams(typing_extensions.TypedDict): + """ + Lifecycle and filesystem details for the result + """ + + updated_at: typing_extensions.Annotated[str, FieldMetadata(alias="updatedAt")] + """ + ISO 8601 timestamp of the latest result update + """ + + created_at: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="createdAt")]] + """ + ISO 8601 timestamp when the result was first created + """ diff --git a/src/supermemory/requests/validation_error_response.py b/src/supermemory/requests/validation_error_response.py new file mode 100644 index 00000000..5ee09ea1 --- /dev/null +++ b/src/supermemory/requests/validation_error_response.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from .validation_error_response_error_item import ValidationErrorResponseErrorItemParams + + +class ValidationErrorResponseParams(typing_extensions.TypedDict): + """ + Body returned when request input fails schema validation + """ + + success: typing.Literal[False] + error: typing.Sequence[ValidationErrorResponseErrorItemParams] + """ + Validation issues + """ + + data: typing_extensions.NotRequired[typing.Any] + """ + The input that failed validation + """ diff --git a/src/supermemory/requests/validation_error_response_error_item.py b/src/supermemory/requests/validation_error_response_error_item.py new file mode 100644 index 00000000..5a3b8138 --- /dev/null +++ b/src/supermemory/requests/validation_error_response_error_item.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from .validation_error_response_error_item_path_item import ValidationErrorResponseErrorItemPathItemParams + + +class ValidationErrorResponseErrorItemParams(typing_extensions.TypedDict): + message: str + path: typing_extensions.NotRequired[typing.Sequence[ValidationErrorResponseErrorItemPathItemParams]] diff --git a/src/supermemory/requests/validation_error_response_error_item_path_item.py b/src/supermemory/requests/validation_error_response_error_item_path_item.py new file mode 100644 index 00000000..f62ff109 --- /dev/null +++ b/src/supermemory/requests/validation_error_response_error_item_path_item.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +ValidationErrorResponseErrorItemPathItemParams = typing.Union[str, float] diff --git a/src/supermemory/requests/web_crawler_config.py b/src/supermemory/requests/web_crawler_config.py new file mode 100644 index 00000000..5d38c11f --- /dev/null +++ b/src/supermemory/requests/web_crawler_config.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class WebCrawlerConfigParams(typing_extensions.TypedDict): + """ + Website to crawl + """ + + start_url: typing_extensions.Annotated[str, FieldMetadata(alias="startUrl")] + crawl_depth: typing_extensions.NotRequired[typing_extensions.Annotated[int, FieldMetadata(alias="crawlDepth")]] + """ + How many links deep to follow. Defaults to 3. + """ diff --git a/src/supermemory/resources/__init__.py b/src/supermemory/resources/__init__.py deleted file mode 100644 index ca24f73f..00000000 --- a/src/supermemory/resources/__init__.py +++ /dev/null @@ -1,75 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from .search import ( - SearchResource, - AsyncSearchResource, - SearchResourceWithRawResponse, - AsyncSearchResourceWithRawResponse, - SearchResourceWithStreamingResponse, - AsyncSearchResourceWithStreamingResponse, -) -from .memories import ( - MemoriesResource, - AsyncMemoriesResource, - MemoriesResourceWithRawResponse, - AsyncMemoriesResourceWithRawResponse, - MemoriesResourceWithStreamingResponse, - AsyncMemoriesResourceWithStreamingResponse, -) -from .settings import ( - SettingsResource, - AsyncSettingsResource, - SettingsResourceWithRawResponse, - AsyncSettingsResourceWithRawResponse, - SettingsResourceWithStreamingResponse, - AsyncSettingsResourceWithStreamingResponse, -) -from .documents import ( - DocumentsResource, - AsyncDocumentsResource, - DocumentsResourceWithRawResponse, - AsyncDocumentsResourceWithRawResponse, - DocumentsResourceWithStreamingResponse, - AsyncDocumentsResourceWithStreamingResponse, -) -from .connections import ( - ConnectionsResource, - AsyncConnectionsResource, - ConnectionsResourceWithRawResponse, - AsyncConnectionsResourceWithRawResponse, - ConnectionsResourceWithStreamingResponse, - AsyncConnectionsResourceWithStreamingResponse, -) - -__all__ = [ - "MemoriesResource", - "AsyncMemoriesResource", - "MemoriesResourceWithRawResponse", - "AsyncMemoriesResourceWithRawResponse", - "MemoriesResourceWithStreamingResponse", - "AsyncMemoriesResourceWithStreamingResponse", - "DocumentsResource", - "AsyncDocumentsResource", - "DocumentsResourceWithRawResponse", - "AsyncDocumentsResourceWithRawResponse", - "DocumentsResourceWithStreamingResponse", - "AsyncDocumentsResourceWithStreamingResponse", - "SearchResource", - "AsyncSearchResource", - "SearchResourceWithRawResponse", - "AsyncSearchResourceWithRawResponse", - "SearchResourceWithStreamingResponse", - "AsyncSearchResourceWithStreamingResponse", - "SettingsResource", - "AsyncSettingsResource", - "SettingsResourceWithRawResponse", - "AsyncSettingsResourceWithRawResponse", - "SettingsResourceWithStreamingResponse", - "AsyncSettingsResourceWithStreamingResponse", - "ConnectionsResource", - "AsyncConnectionsResource", - "ConnectionsResourceWithRawResponse", - "AsyncConnectionsResourceWithRawResponse", - "ConnectionsResourceWithStreamingResponse", - "AsyncConnectionsResourceWithStreamingResponse", -] diff --git a/src/supermemory/resources/connections.py b/src/supermemory/resources/connections.py deleted file mode 100644 index c3910fb3..00000000 --- a/src/supermemory/resources/connections.py +++ /dev/null @@ -1,1028 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Dict, Union, Iterable, Optional -from typing_extensions import Literal - -import httpx - -from ..types import ( - connection_list_params, - connection_create_params, - connection_import_params, - connection_configure_params, - connection_resources_params, - connection_get_by_tag_params, - connection_delete_by_id_params, - connection_list_documents_params, - connection_delete_by_provider_params, -) -from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given -from .._utils import path_template, 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.connection_list_response import ConnectionListResponse -from ..types.connection_create_response import ConnectionCreateResponse -from ..types.connection_configure_response import ConnectionConfigureResponse -from ..types.connection_get_by_id_response import ConnectionGetByIDResponse -from ..types.connection_resources_response import ConnectionResourcesResponse -from ..types.connection_get_by_tag_response import ConnectionGetByTagResponse -from ..types.connection_delete_by_id_response import ConnectionDeleteByIDResponse -from ..types.connection_list_documents_response import ConnectionListDocumentsResponse -from ..types.connection_delete_by_provider_response import ConnectionDeleteByProviderResponse - -__all__ = ["ConnectionsResource", "AsyncConnectionsResource"] - - -class ConnectionsResource(SyncAPIResource): - """External service integrations""" - - @cached_property - def with_raw_response(self) -> ConnectionsResourceWithRawResponse: - """ - 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 ConnectionsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> ConnectionsResourceWithStreamingResponse: - """ - 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 ConnectionsResourceWithStreamingResponse(self) - - def create( - self, - provider: Literal["notion", "google-drive", "onedrive", "gmail", "github", "web-crawler", "s3", "granola"], - *, - container_tag: str | Omit = omit, - container_tags: SequenceNotStr[str] | Omit = omit, - document_limit: int | Omit = omit, - metadata: Optional[Dict[str, Union[str, float, bool]]] | Omit = omit, - redirect_url: 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, - ) -> ConnectionCreateResponse: - """ - Initialize connection and get authorization URL - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not provider: - raise ValueError(f"Expected a non-empty value for `provider` but received {provider!r}") - return self._post( - path_template("/v3/connections/{provider}", provider=provider), - body=maybe_transform( - { - "container_tag": container_tag, - "container_tags": container_tags, - "document_limit": document_limit, - "metadata": metadata, - "redirect_url": redirect_url, - }, - connection_create_params.ConnectionCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ConnectionCreateResponse, - ) - - def list( - self, - *, - 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. - # 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, - ) -> ConnectionListResponse: - """ - List all connections - - Args: - container_tags: Optional comma-separated list of container tags to filter documents by - - 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( - "/v3/connections/list", - body=maybe_transform({"container_tags": container_tags}, connection_list_params.ConnectionListParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ConnectionListResponse, - ) - - def configure( - self, - connection_id: str, - *, - resources: Iterable[Dict[str, object]], - # 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, - ) -> ConnectionConfigureResponse: - """ - Configure resources for a connection (supported providers: GitHub for now) - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not connection_id: - raise ValueError(f"Expected a non-empty value for `connection_id` but received {connection_id!r}") - return self._post( - path_template("/v3/connections/{connection_id}/configure", connection_id=connection_id), - body=maybe_transform({"resources": resources}, connection_configure_params.ConnectionConfigureParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ConnectionConfigureResponse, - ) - - def delete_by_id( - self, - connection_id: str, - *, - delete_documents: 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, - ) -> ConnectionDeleteByIDResponse: - """ - Delete a specific connection by ID - - Args: - delete_documents: Whether to also delete documents imported by this connection. Defaults to true. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not connection_id: - raise ValueError(f"Expected a non-empty value for `connection_id` but received {connection_id!r}") - return self._delete( - path_template("/v3/connections/{connection_id}", connection_id=connection_id), - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform( - {"delete_documents": delete_documents}, connection_delete_by_id_params.ConnectionDeleteByIDParams - ), - ), - cast_to=ConnectionDeleteByIDResponse, - ) - - def delete_by_provider( - self, - provider: Literal["notion", "google-drive", "onedrive", "gmail", "github", "web-crawler", "s3", "granola"], - *, - container_tags: SequenceNotStr[str], - # 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, - ) -> ConnectionDeleteByProviderResponse: - """ - Delete connection for a specific provider and container tags - - Args: - container_tags: Optional comma-separated list of container tags to filter connections by - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not provider: - raise ValueError(f"Expected a non-empty value for `provider` but received {provider!r}") - return self._delete( - path_template("/v3/connections/{provider}", provider=provider), - body=maybe_transform( - {"container_tags": container_tags}, - connection_delete_by_provider_params.ConnectionDeleteByProviderParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ConnectionDeleteByProviderResponse, - ) - - def get_by_id( - self, - connection_id: str, - *, - # 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, - ) -> ConnectionGetByIDResponse: - """ - Get connection details with id - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not connection_id: - raise ValueError(f"Expected a non-empty value for `connection_id` but received {connection_id!r}") - return self._get( - path_template("/v3/connections/{connection_id}", connection_id=connection_id), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ConnectionGetByIDResponse, - ) - - def get_by_tag( - self, - provider: Literal["notion", "google-drive", "onedrive", "gmail", "github", "web-crawler", "s3", "granola"], - *, - container_tags: SequenceNotStr[str], - # 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, - ) -> ConnectionGetByTagResponse: - """ - Get connection details with provider and container tags - - Args: - container_tags: Comma-separated list of container tags to filter connection by - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not provider: - raise ValueError(f"Expected a non-empty value for `provider` but received {provider!r}") - return self._post( - path_template("/v3/connections/{provider}/connection", provider=provider), - body=maybe_transform( - {"container_tags": container_tags}, connection_get_by_tag_params.ConnectionGetByTagParams - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ConnectionGetByTagResponse, - ) - - def import_( - self, - provider: Literal["notion", "google-drive", "onedrive", "gmail", "github", "web-crawler", "s3", "granola"], - *, - 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. - # 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, - ) -> str: - """ - Initiate a manual sync of connections - - Args: - container_tags: Optional comma-separated list of container tags to filter connections by - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not provider: - raise ValueError(f"Expected a non-empty value for `provider` but received {provider!r}") - extra_headers = {"Accept": "text/plain", **(extra_headers or {})} - return self._post( - path_template("/v3/connections/{provider}/import", provider=provider), - body=maybe_transform({"container_tags": container_tags}, connection_import_params.ConnectionImportParams), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=str, - ) - - def list_documents( - self, - provider: Literal["notion", "google-drive", "onedrive", "gmail", "github", "web-crawler", "s3", "granola"], - *, - 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. - # 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, - ) -> ConnectionListDocumentsResponse: - """ - List documents indexed for a provider and container tags - - Args: - container_tags: Optional comma-separated list of container tags to filter documents by - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not provider: - raise ValueError(f"Expected a non-empty value for `provider` but received {provider!r}") - return self._post( - path_template("/v3/connections/{provider}/documents", provider=provider), - body=maybe_transform( - {"container_tags": container_tags}, connection_list_documents_params.ConnectionListDocumentsParams - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ConnectionListDocumentsResponse, - ) - - def resources( - self, - connection_id: str, - *, - page: float | Omit = omit, - parent_id: str | Omit = omit, - per_page: float | 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, - ) -> ConnectionResourcesResponse: - """ - Fetch resources for a connection (supported providers: GitHub for now) - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not connection_id: - raise ValueError(f"Expected a non-empty value for `connection_id` but received {connection_id!r}") - return self._get( - path_template("/v3/connections/{connection_id}/resources", connection_id=connection_id), - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform( - { - "page": page, - "parent_id": parent_id, - "per_page": per_page, - }, - connection_resources_params.ConnectionResourcesParams, - ), - ), - cast_to=ConnectionResourcesResponse, - ) - - -class AsyncConnectionsResource(AsyncAPIResource): - """External service integrations""" - - @cached_property - def with_raw_response(self) -> AsyncConnectionsResourceWithRawResponse: - """ - 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 AsyncConnectionsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncConnectionsResourceWithStreamingResponse: - """ - 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 AsyncConnectionsResourceWithStreamingResponse(self) - - async def create( - self, - provider: Literal["notion", "google-drive", "onedrive", "gmail", "github", "web-crawler", "s3", "granola"], - *, - container_tag: str | Omit = omit, - container_tags: SequenceNotStr[str] | Omit = omit, - document_limit: int | Omit = omit, - metadata: Optional[Dict[str, Union[str, float, bool]]] | Omit = omit, - redirect_url: 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, - ) -> ConnectionCreateResponse: - """ - Initialize connection and get authorization URL - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not provider: - raise ValueError(f"Expected a non-empty value for `provider` but received {provider!r}") - return await self._post( - path_template("/v3/connections/{provider}", provider=provider), - body=await async_maybe_transform( - { - "container_tag": container_tag, - "container_tags": container_tags, - "document_limit": document_limit, - "metadata": metadata, - "redirect_url": redirect_url, - }, - connection_create_params.ConnectionCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ConnectionCreateResponse, - ) - - async def list( - self, - *, - 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. - # 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, - ) -> ConnectionListResponse: - """ - List all connections - - Args: - container_tags: Optional comma-separated list of container tags to filter documents by - - 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( - "/v3/connections/list", - body=await async_maybe_transform( - {"container_tags": container_tags}, connection_list_params.ConnectionListParams - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ConnectionListResponse, - ) - - async def configure( - self, - connection_id: str, - *, - resources: Iterable[Dict[str, object]], - # 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, - ) -> ConnectionConfigureResponse: - """ - Configure resources for a connection (supported providers: GitHub for now) - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not connection_id: - raise ValueError(f"Expected a non-empty value for `connection_id` but received {connection_id!r}") - return await self._post( - path_template("/v3/connections/{connection_id}/configure", connection_id=connection_id), - body=await async_maybe_transform( - {"resources": resources}, connection_configure_params.ConnectionConfigureParams - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ConnectionConfigureResponse, - ) - - async def delete_by_id( - self, - connection_id: str, - *, - delete_documents: 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, - ) -> ConnectionDeleteByIDResponse: - """ - Delete a specific connection by ID - - Args: - delete_documents: Whether to also delete documents imported by this connection. Defaults to true. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not connection_id: - raise ValueError(f"Expected a non-empty value for `connection_id` but received {connection_id!r}") - return await self._delete( - path_template("/v3/connections/{connection_id}", connection_id=connection_id), - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=await async_maybe_transform( - {"delete_documents": delete_documents}, connection_delete_by_id_params.ConnectionDeleteByIDParams - ), - ), - cast_to=ConnectionDeleteByIDResponse, - ) - - async def delete_by_provider( - self, - provider: Literal["notion", "google-drive", "onedrive", "gmail", "github", "web-crawler", "s3", "granola"], - *, - container_tags: SequenceNotStr[str], - # 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, - ) -> ConnectionDeleteByProviderResponse: - """ - Delete connection for a specific provider and container tags - - Args: - container_tags: Optional comma-separated list of container tags to filter connections by - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not provider: - raise ValueError(f"Expected a non-empty value for `provider` but received {provider!r}") - return await self._delete( - path_template("/v3/connections/{provider}", provider=provider), - body=await async_maybe_transform( - {"container_tags": container_tags}, - connection_delete_by_provider_params.ConnectionDeleteByProviderParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ConnectionDeleteByProviderResponse, - ) - - async def get_by_id( - self, - connection_id: str, - *, - # 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, - ) -> ConnectionGetByIDResponse: - """ - Get connection details with id - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not connection_id: - raise ValueError(f"Expected a non-empty value for `connection_id` but received {connection_id!r}") - return await self._get( - path_template("/v3/connections/{connection_id}", connection_id=connection_id), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ConnectionGetByIDResponse, - ) - - async def get_by_tag( - self, - provider: Literal["notion", "google-drive", "onedrive", "gmail", "github", "web-crawler", "s3", "granola"], - *, - container_tags: SequenceNotStr[str], - # 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, - ) -> ConnectionGetByTagResponse: - """ - Get connection details with provider and container tags - - Args: - container_tags: Comma-separated list of container tags to filter connection by - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not provider: - raise ValueError(f"Expected a non-empty value for `provider` but received {provider!r}") - return await self._post( - path_template("/v3/connections/{provider}/connection", provider=provider), - body=await async_maybe_transform( - {"container_tags": container_tags}, connection_get_by_tag_params.ConnectionGetByTagParams - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ConnectionGetByTagResponse, - ) - - async def import_( - self, - provider: Literal["notion", "google-drive", "onedrive", "gmail", "github", "web-crawler", "s3", "granola"], - *, - 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. - # 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, - ) -> str: - """ - Initiate a manual sync of connections - - Args: - container_tags: Optional comma-separated list of container tags to filter connections by - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not provider: - raise ValueError(f"Expected a non-empty value for `provider` but received {provider!r}") - extra_headers = {"Accept": "text/plain", **(extra_headers or {})} - return await self._post( - path_template("/v3/connections/{provider}/import", provider=provider), - body=await async_maybe_transform( - {"container_tags": container_tags}, connection_import_params.ConnectionImportParams - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=str, - ) - - async def list_documents( - self, - provider: Literal["notion", "google-drive", "onedrive", "gmail", "github", "web-crawler", "s3", "granola"], - *, - 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. - # 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, - ) -> ConnectionListDocumentsResponse: - """ - List documents indexed for a provider and container tags - - Args: - container_tags: Optional comma-separated list of container tags to filter documents by - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not provider: - raise ValueError(f"Expected a non-empty value for `provider` but received {provider!r}") - return await self._post( - path_template("/v3/connections/{provider}/documents", provider=provider), - body=await async_maybe_transform( - {"container_tags": container_tags}, connection_list_documents_params.ConnectionListDocumentsParams - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=ConnectionListDocumentsResponse, - ) - - async def resources( - self, - connection_id: str, - *, - page: float | Omit = omit, - parent_id: str | Omit = omit, - per_page: float | 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, - ) -> ConnectionResourcesResponse: - """ - Fetch resources for a connection (supported providers: GitHub for now) - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not connection_id: - raise ValueError(f"Expected a non-empty value for `connection_id` but received {connection_id!r}") - return await self._get( - path_template("/v3/connections/{connection_id}/resources", connection_id=connection_id), - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=await async_maybe_transform( - { - "page": page, - "parent_id": parent_id, - "per_page": per_page, - }, - connection_resources_params.ConnectionResourcesParams, - ), - ), - cast_to=ConnectionResourcesResponse, - ) - - -class ConnectionsResourceWithRawResponse: - def __init__(self, connections: ConnectionsResource) -> None: - self._connections = connections - - self.create = to_raw_response_wrapper( - connections.create, - ) - self.list = to_raw_response_wrapper( - connections.list, - ) - self.configure = to_raw_response_wrapper( - connections.configure, - ) - self.delete_by_id = to_raw_response_wrapper( - connections.delete_by_id, - ) - self.delete_by_provider = to_raw_response_wrapper( - connections.delete_by_provider, - ) - self.get_by_id = to_raw_response_wrapper( - connections.get_by_id, - ) - self.get_by_tag = to_raw_response_wrapper( - connections.get_by_tag, - ) - self.import_ = to_raw_response_wrapper( - connections.import_, - ) - self.list_documents = to_raw_response_wrapper( - connections.list_documents, - ) - self.resources = to_raw_response_wrapper( - connections.resources, - ) - - -class AsyncConnectionsResourceWithRawResponse: - def __init__(self, connections: AsyncConnectionsResource) -> None: - self._connections = connections - - self.create = async_to_raw_response_wrapper( - connections.create, - ) - self.list = async_to_raw_response_wrapper( - connections.list, - ) - self.configure = async_to_raw_response_wrapper( - connections.configure, - ) - self.delete_by_id = async_to_raw_response_wrapper( - connections.delete_by_id, - ) - self.delete_by_provider = async_to_raw_response_wrapper( - connections.delete_by_provider, - ) - self.get_by_id = async_to_raw_response_wrapper( - connections.get_by_id, - ) - self.get_by_tag = async_to_raw_response_wrapper( - connections.get_by_tag, - ) - self.import_ = async_to_raw_response_wrapper( - connections.import_, - ) - self.list_documents = async_to_raw_response_wrapper( - connections.list_documents, - ) - self.resources = async_to_raw_response_wrapper( - connections.resources, - ) - - -class ConnectionsResourceWithStreamingResponse: - def __init__(self, connections: ConnectionsResource) -> None: - self._connections = connections - - self.create = to_streamed_response_wrapper( - connections.create, - ) - self.list = to_streamed_response_wrapper( - connections.list, - ) - self.configure = to_streamed_response_wrapper( - connections.configure, - ) - self.delete_by_id = to_streamed_response_wrapper( - connections.delete_by_id, - ) - self.delete_by_provider = to_streamed_response_wrapper( - connections.delete_by_provider, - ) - self.get_by_id = to_streamed_response_wrapper( - connections.get_by_id, - ) - self.get_by_tag = to_streamed_response_wrapper( - connections.get_by_tag, - ) - self.import_ = to_streamed_response_wrapper( - connections.import_, - ) - self.list_documents = to_streamed_response_wrapper( - connections.list_documents, - ) - self.resources = to_streamed_response_wrapper( - connections.resources, - ) - - -class AsyncConnectionsResourceWithStreamingResponse: - def __init__(self, connections: AsyncConnectionsResource) -> None: - self._connections = connections - - self.create = async_to_streamed_response_wrapper( - connections.create, - ) - self.list = async_to_streamed_response_wrapper( - connections.list, - ) - self.configure = async_to_streamed_response_wrapper( - connections.configure, - ) - self.delete_by_id = async_to_streamed_response_wrapper( - connections.delete_by_id, - ) - self.delete_by_provider = async_to_streamed_response_wrapper( - connections.delete_by_provider, - ) - self.get_by_id = async_to_streamed_response_wrapper( - connections.get_by_id, - ) - self.get_by_tag = async_to_streamed_response_wrapper( - connections.get_by_tag, - ) - self.import_ = async_to_streamed_response_wrapper( - connections.import_, - ) - self.list_documents = async_to_streamed_response_wrapper( - connections.list_documents, - ) - self.resources = async_to_streamed_response_wrapper( - connections.resources, - ) diff --git a/src/supermemory/resources/documents.py b/src/supermemory/resources/documents.py deleted file mode 100644 index 6e84b362..00000000 --- a/src/supermemory/resources/documents.py +++ /dev/null @@ -1,1510 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Dict, Union, Mapping, Iterable, cast -from typing_extensions import Literal - -import httpx - -from ..types import ( - document_add_params, - document_list_params, - document_update_params, - document_batch_add_params, - document_delete_bulk_params, - document_upload_file_params, - document_list_processing_params, -) -from .._files import deepcopy_with_paths -from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, FileTypes, SequenceNotStr, omit, not_given -from .._utils import extract_files, path_template, 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.document_add_response import DocumentAddResponse -from ..types.document_get_response import DocumentGetResponse -from ..types.document_list_response import DocumentListResponse -from ..types.document_update_response import DocumentUpdateResponse -from ..types.document_batch_add_response import DocumentBatchAddResponse -from ..types.document_delete_bulk_response import DocumentDeleteBulkResponse -from ..types.document_upload_file_response import DocumentUploadFileResponse -from ..types.document_list_processing_response import DocumentListProcessingResponse - -__all__ = ["DocumentsResource", "AsyncDocumentsResource"] - - -class DocumentsResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> DocumentsResourceWithRawResponse: - """ - 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 DocumentsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> DocumentsResourceWithStreamingResponse: - """ - 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 DocumentsResourceWithStreamingResponse(self) - - def update( - self, - id: str, - *, - container_tag: str | Omit = omit, - container_tags: SequenceNotStr[str] | Omit = omit, - content: str | Omit = omit, - custom_id: str | Omit = omit, - document_date: str | Omit = omit, - filepath: str | Omit = omit, - filter_by_metadata: Dict[str, Union[str, float, bool, SequenceNotStr[str]]] | Omit = omit, - metadata: Dict[str, Union[str, float, bool, SequenceNotStr[str]]] | Omit = omit, - task_type: Literal["memory", "superrag"] | 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, - ) -> DocumentUpdateResponse: - """ - Update a document with any content type (text, url, file, etc.) and metadata - - Args: - 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. - - 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. - - custom_id: Optional custom ID of the document. This could be an ID from your database that - will uniquely identify this document. - - document_date: When this document's content is from, as opposed to when it was uploaded. - Accepts YYYY-MM-DD or a full ISO 8601 timestamp. Memory extraction resolves - relative dates ('yesterday', 'last quarter') against this instead of the - ingestion time, and documents in a batch are processed oldest-first so newer - facts correctly supersede older ones. Set this whenever you backfill historical - content. - - filepath: Optional file path for the document (e.g., '/documents/reports/file.pdf'). Used - by supermemoryfs to map documents to filesystem paths. - - filter_by_metadata: Optional metadata filter scoping which existing memories are pulled as context - during ingestion. Scalar values match exactly (AND across keys); array values - match ANY (OR within key). Only memories whose source documents match this - filter are used as context. - - 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. - - task_type: Task type: "memory" (default) for full context layer with SuperRAG built in, - "superrag" for managed RAG as a service. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - return self._patch( - path_template("/v3/documents/{id}", id=id), - body=maybe_transform( - { - "container_tag": container_tag, - "container_tags": container_tags, - "content": content, - "custom_id": custom_id, - "document_date": document_date, - "filepath": filepath, - "filter_by_metadata": filter_by_metadata, - "metadata": metadata, - "task_type": task_type, - }, - document_update_params.DocumentUpdateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=DocumentUpdateResponse, - ) - - def list( - self, - *, - container_tags: SequenceNotStr[str] | Omit = omit, - filepath: str | Omit = omit, - filters: document_list_params.Filters | Omit = omit, - include_content: bool | Omit = omit, - limit: Union[str, float] | Omit = omit, - order: Literal["asc", "desc"] | Omit = omit, - page: Union[str, float] | Omit = omit, - sort: Literal["createdAt", "updatedAt"] | 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, - ) -> DocumentListResponse: - """ - Retrieves a paginated list of documents with their metadata and workflow status - - Args: - container_tags: 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. - - filepath: Filter documents by filepath. Exact match for full paths, prefix match if ending - with / - - filters: Optional filters to apply to the search. Can be a JSON string or Query object. - - include_content: Whether to include the content field in the response. Warning: This can make - responses significantly larger. - - limit: Number of items per page - - order: Sort order - - page: Page number to fetch - - sort: Field to sort by - - 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( - "/v3/documents/list", - body=maybe_transform( - { - "container_tags": container_tags, - "filepath": filepath, - "filters": filters, - "include_content": include_content, - "limit": limit, - "order": order, - "page": page, - "sort": sort, - }, - document_list_params.DocumentListParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=DocumentListResponse, - ) - - def delete( - self, - id: str, - *, - # 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, - ) -> None: - """ - Delete a document by ID or customId - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return self._delete( - path_template("/v3/documents/{id}", id=id), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - - def add( - self, - *, - content: str, - container_tag: str | Omit = omit, - container_tags: SequenceNotStr[str] | Omit = omit, - custom_id: str | Omit = omit, - document_date: str | Omit = omit, - dreaming: Literal["instant", "dynamic"] | Omit = omit, - entity_context: str | Omit = omit, - filepath: str | Omit = omit, - filter_by_metadata: Dict[str, Union[str, float, bool, SequenceNotStr[str]]] | Omit = omit, - metadata: Dict[str, Union[str, float, bool, SequenceNotStr[str]]] | Omit = omit, - task_type: Literal["memory", "superrag"] | 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, - ) -> DocumentAddResponse: - """ - Add a document with any content type (text, url, file, etc.) and metadata - - Args: - 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. - - container_tag: Optional tag this document should be containerized by. Max 100 characters, - alphanumeric with hyphens, underscores, and dots only. - - custom_id: Optional custom ID of the document. Max 100 characters, alphanumeric with - hyphens, underscores, and dots only. - - document_date: When this document's content is from, as opposed to when it was uploaded. - Accepts YYYY-MM-DD or a full ISO 8601 timestamp. Memory extraction resolves - relative dates against this instead of the ingestion time, and documents in a - batch are processed oldest-first so newer facts correctly supersede older ones. - Set this whenever you backfill historical content. - - dreaming: Processing mode. "dynamic" (default) groups related documents together so - memories form from coherent, logical units rather than one isolated entry at a - time. "instant" processes each document on its own right away, and bills one - extra operation per document. - - entity_context: Optional entity context for this container tag. Max 1500 characters. Used during - document processing to guide memory extraction. - - filepath: Optional file path for the document. Used by supermemoryfs to store the full - path of the file. - - filter_by_metadata: Optional metadata filter to apply when pulling related memories and profile - during ingestion. Only memories matching these filters will be used as context. - - metadata: Optional metadata for the document. - - task_type: Task type: "memory" (default) for full context layer with SuperRAG built in, - "superrag" for managed RAG as a service. - - 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( - "/v3/documents", - body=maybe_transform( - { - "content": content, - "container_tag": container_tag, - "container_tags": container_tags, - "custom_id": custom_id, - "document_date": document_date, - "dreaming": dreaming, - "entity_context": entity_context, - "filepath": filepath, - "filter_by_metadata": filter_by_metadata, - "metadata": metadata, - "task_type": task_type, - }, - document_add_params.DocumentAddParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=DocumentAddResponse, - ) - - def batch_add( - self, - *, - documents: Union[Iterable[document_batch_add_params.DocumentsUnionMember0], SequenceNotStr[str]], - container_tag: str | Omit = omit, - container_tags: SequenceNotStr[str] | Omit = omit, - content: None | Omit = omit, - document_date: str | Omit = omit, - dreaming: Literal["instant", "dynamic"] | Omit = omit, - entity_context: str | Omit = omit, - filepath: str | Omit = omit, - filter_by_metadata: Dict[str, Union[str, float, bool, SequenceNotStr[str]]] | Omit = omit, - metadata: Dict[str, Union[str, float, bool, SequenceNotStr[str]]] | Omit = omit, - task_type: Literal["memory", "superrag"] | 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, - ) -> DocumentBatchAddResponse: - """Add multiple documents in a single request. - - Each document can have any content - type (text, url, file, etc.) and metadata - - Args: - 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. - - document_date: When this document's content is from, as opposed to when it was uploaded. - Accepts YYYY-MM-DD or a full ISO 8601 timestamp. Memory extraction resolves - relative dates ('yesterday', 'last quarter') against this instead of the - ingestion time, and documents in a batch are processed oldest-first so newer - facts correctly supersede older ones. Set this whenever you backfill historical - content. - - dreaming: Processing mode. "dynamic" (default) groups related documents together so - memories form from coherent, logical units rather than one isolated entry at a - time. "instant" processes each document on its own right away, and bills one - extra operation per document. - - entity_context: Optional entity context for this container tag. Max 1500 characters. Used during - document processing to guide memory extraction. - - filepath: Optional file path for the document (e.g., '/documents/reports/file.pdf'). Used - by supermemoryfs to map documents to filesystem paths. - - filter_by_metadata: Optional metadata filter scoping which existing memories are pulled as context - during ingestion. Scalar values match exactly (AND across keys); array values - match ANY (OR within key). Only memories whose source documents match this - filter are used as context. - - 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. - - task_type: Task type: "memory" (default) for full context layer with SuperRAG built in, - "superrag" for managed RAG as a service. - - 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( - "/v3/documents/batch", - body=maybe_transform( - { - "documents": documents, - "container_tag": container_tag, - "container_tags": container_tags, - "content": content, - "document_date": document_date, - "dreaming": dreaming, - "entity_context": entity_context, - "filepath": filepath, - "filter_by_metadata": filter_by_metadata, - "metadata": metadata, - "task_type": task_type, - }, - document_batch_add_params.DocumentBatchAddParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=DocumentBatchAddResponse, - ) - - def delete_bulk( - self, - *, - container_tags: SequenceNotStr[str] | Omit = omit, - filepath: str | Omit = omit, - ids: SequenceNotStr[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, - ) -> DocumentDeleteBulkResponse: - """ - Bulk delete documents by IDs or container tags - - Args: - container_tags: Array of container tags - all documents in these containers will be deleted - - filepath: Delete documents matching this filepath. Exact match for full paths, prefix - match if ending with / - - ids: Array of document IDs to delete (max 100 at once) - - 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._delete( - "/v3/documents/bulk", - body=maybe_transform( - { - "container_tags": container_tags, - "filepath": filepath, - "ids": ids, - }, - document_delete_bulk_params.DocumentDeleteBulkParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=DocumentDeleteBulkResponse, - ) - - def get( - self, - id: str, - *, - # 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, - ) -> DocumentGetResponse: - """ - Get a document by ID - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - return self._get( - path_template("/v3/documents/{id}", id=id), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=DocumentGetResponse, - ) - - def list_processing( - self, - *, - container_tags: str | Omit = omit, - limit: Union[str, float] | Omit = omit, - page: Union[str, float] | Omit = omit, - view: Literal["active", "pending", "all"] | 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, - ) -> DocumentListProcessingResponse: - """Get documents that are currently being processed. - - Default `view=active` is the - live in-flight queue. `view=pending` is every unfinished document with no time - cutoff. `view=all` also includes failed documents, paginated. - - Args: - container_tags: Comma-separated container tags to filter by - - limit: Number of items per page. Used with `view=all`. - - page: Page number to fetch. Used with `view=all`. - - view: `active` returns in-flight documents updated in the last 4 hours. `pending` - returns every document that is not done or failed, with no time cutoff. `all` - also includes failed documents, paginated. - - 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._get( - "/v3/documents/processing", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform( - { - "container_tags": container_tags, - "limit": limit, - "page": page, - "view": view, - }, - document_list_processing_params.DocumentListProcessingParams, - ), - ), - cast_to=DocumentListProcessingResponse, - ) - - def upload_file( - self, - *, - file: FileTypes, - container_tag: str | Omit = omit, - container_tags: str | Omit = omit, - custom_id: str | Omit = omit, - dreaming: Literal["instant", "dynamic"] | Omit = omit, - entity_context: str | Omit = omit, - filepath: str | Omit = omit, - file_type: str | Omit = omit, - filter_by_metadata: str | Omit = omit, - metadata: str | Omit = omit, - mime_type: str | Omit = omit, - task_type: Literal["memory", "superrag"] | Omit = omit, - use_advanced_processing: 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, - ) -> DocumentUploadFileResponse: - """ - Upload a file to be processed - - Args: - file: File to upload and process - - container_tag: Optional container tag (e.g., 'user_123'). Use this for a single tag. - - container_tags: Optional container tags. Can be either a JSON string of an array (e.g., - '["user_123", "project_123"]') or a single string (e.g., 'user_123'). Single - strings will be automatically converted to an array. - - custom_id: Optional custom ID of the document. Max 100 characters, alphanumeric with - hyphens, underscores, and colons only. - - dreaming: Processing mode. "dynamic" (default) groups related documents together so - memories form from coherent, logical units rather than one isolated entry at a - time. "instant" processes each document on its own right away, and bills one - extra operation per document. - - entity_context: Optional entity context for this container tag. Max 1500 characters. Used during - document processing to guide memory extraction. - - filepath: Optional file path for the uploaded file (e.g., '/documents/reports/file.pdf'). - Used by supermemoryfs to map documents to filesystem paths. - - file_type: - Optional file type override to force specific processing behavior. Valid values: - text, pdf, tweet, google_doc, google_slide, google_sheet, image, video, - notion_doc, webpage, onedrive - - filter_by_metadata: Optional metadata filter as a JSON string. Scopes which existing memories are - pulled as context during ingestion. Scalar values match exactly (AND across - keys); array values match ANY (OR within key). - - metadata: Optional metadata for the document as a JSON string. This is used to store - additional information about the document. Keys must be strings and values can - be strings, numbers, or booleans. - - mime_type: Required when fileType is 'image' or 'video'. Specifies the exact MIME type to - use (e.g., 'image/png', 'image/jpeg', 'video/mp4', 'video/webm') - - task_type: Task type: "memory" (default) for full context layer with SuperRAG built in, - "superrag" for managed RAG as a service. - - use_advanced_processing: DEPRECATED: This field is no longer used. Advanced PDF processing is now - automatic with our hybrid Mistral OCR + Gemini pipeline. This parameter will be - accepted but ignored for backwards compatibility. - - 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_with_paths( - { - "file": file, - "container_tag": container_tag, - "container_tags": container_tags, - "custom_id": custom_id, - "dreaming": dreaming, - "entity_context": entity_context, - "filepath": filepath, - "file_type": file_type, - "filter_by_metadata": filter_by_metadata, - "metadata": metadata, - "mime_type": mime_type, - "task_type": task_type, - "use_advanced_processing": use_advanced_processing, - }, - [["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/documents/file", - body=maybe_transform(body, document_upload_file_params.DocumentUploadFileParams), - files=files, - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=DocumentUploadFileResponse, - ) - - -class AsyncDocumentsResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncDocumentsResourceWithRawResponse: - """ - 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 AsyncDocumentsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncDocumentsResourceWithStreamingResponse: - """ - 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 AsyncDocumentsResourceWithStreamingResponse(self) - - async def update( - self, - id: str, - *, - container_tag: str | Omit = omit, - container_tags: SequenceNotStr[str] | Omit = omit, - content: str | Omit = omit, - custom_id: str | Omit = omit, - document_date: str | Omit = omit, - filepath: str | Omit = omit, - filter_by_metadata: Dict[str, Union[str, float, bool, SequenceNotStr[str]]] | Omit = omit, - metadata: Dict[str, Union[str, float, bool, SequenceNotStr[str]]] | Omit = omit, - task_type: Literal["memory", "superrag"] | 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, - ) -> DocumentUpdateResponse: - """ - Update a document with any content type (text, url, file, etc.) and metadata - - Args: - 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. - - 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. - - custom_id: Optional custom ID of the document. This could be an ID from your database that - will uniquely identify this document. - - document_date: When this document's content is from, as opposed to when it was uploaded. - Accepts YYYY-MM-DD or a full ISO 8601 timestamp. Memory extraction resolves - relative dates ('yesterday', 'last quarter') against this instead of the - ingestion time, and documents in a batch are processed oldest-first so newer - facts correctly supersede older ones. Set this whenever you backfill historical - content. - - filepath: Optional file path for the document (e.g., '/documents/reports/file.pdf'). Used - by supermemoryfs to map documents to filesystem paths. - - filter_by_metadata: Optional metadata filter scoping which existing memories are pulled as context - during ingestion. Scalar values match exactly (AND across keys); array values - match ANY (OR within key). Only memories whose source documents match this - filter are used as context. - - 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. - - task_type: Task type: "memory" (default) for full context layer with SuperRAG built in, - "superrag" for managed RAG as a service. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - return await self._patch( - path_template("/v3/documents/{id}", id=id), - body=await async_maybe_transform( - { - "container_tag": container_tag, - "container_tags": container_tags, - "content": content, - "custom_id": custom_id, - "document_date": document_date, - "filepath": filepath, - "filter_by_metadata": filter_by_metadata, - "metadata": metadata, - "task_type": task_type, - }, - document_update_params.DocumentUpdateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=DocumentUpdateResponse, - ) - - async def list( - self, - *, - container_tags: SequenceNotStr[str] | Omit = omit, - filepath: str | Omit = omit, - filters: document_list_params.Filters | Omit = omit, - include_content: bool | Omit = omit, - limit: Union[str, float] | Omit = omit, - order: Literal["asc", "desc"] | Omit = omit, - page: Union[str, float] | Omit = omit, - sort: Literal["createdAt", "updatedAt"] | 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, - ) -> DocumentListResponse: - """ - Retrieves a paginated list of documents with their metadata and workflow status - - Args: - container_tags: 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. - - filepath: Filter documents by filepath. Exact match for full paths, prefix match if ending - with / - - filters: Optional filters to apply to the search. Can be a JSON string or Query object. - - include_content: Whether to include the content field in the response. Warning: This can make - responses significantly larger. - - limit: Number of items per page - - order: Sort order - - page: Page number to fetch - - sort: Field to sort by - - 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( - "/v3/documents/list", - body=await async_maybe_transform( - { - "container_tags": container_tags, - "filepath": filepath, - "filters": filters, - "include_content": include_content, - "limit": limit, - "order": order, - "page": page, - "sort": sort, - }, - document_list_params.DocumentListParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=DocumentListResponse, - ) - - async def delete( - self, - id: str, - *, - # 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, - ) -> None: - """ - Delete a document by ID or customId - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return await self._delete( - path_template("/v3/documents/{id}", id=id), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - - async def add( - self, - *, - content: str, - container_tag: str | Omit = omit, - container_tags: SequenceNotStr[str] | Omit = omit, - custom_id: str | Omit = omit, - document_date: str | Omit = omit, - dreaming: Literal["instant", "dynamic"] | Omit = omit, - entity_context: str | Omit = omit, - filepath: str | Omit = omit, - filter_by_metadata: Dict[str, Union[str, float, bool, SequenceNotStr[str]]] | Omit = omit, - metadata: Dict[str, Union[str, float, bool, SequenceNotStr[str]]] | Omit = omit, - task_type: Literal["memory", "superrag"] | 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, - ) -> DocumentAddResponse: - """ - Add a document with any content type (text, url, file, etc.) and metadata - - Args: - 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. - - container_tag: Optional tag this document should be containerized by. Max 100 characters, - alphanumeric with hyphens, underscores, and dots only. - - custom_id: Optional custom ID of the document. Max 100 characters, alphanumeric with - hyphens, underscores, and dots only. - - document_date: When this document's content is from, as opposed to when it was uploaded. - Accepts YYYY-MM-DD or a full ISO 8601 timestamp. Memory extraction resolves - relative dates against this instead of the ingestion time, and documents in a - batch are processed oldest-first so newer facts correctly supersede older ones. - Set this whenever you backfill historical content. - - dreaming: Processing mode. "dynamic" (default) groups related documents together so - memories form from coherent, logical units rather than one isolated entry at a - time. "instant" processes each document on its own right away, and bills one - extra operation per document. - - entity_context: Optional entity context for this container tag. Max 1500 characters. Used during - document processing to guide memory extraction. - - filepath: Optional file path for the document. Used by supermemoryfs to store the full - path of the file. - - filter_by_metadata: Optional metadata filter to apply when pulling related memories and profile - during ingestion. Only memories matching these filters will be used as context. - - metadata: Optional metadata for the document. - - task_type: Task type: "memory" (default) for full context layer with SuperRAG built in, - "superrag" for managed RAG as a service. - - 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( - "/v3/documents", - body=await async_maybe_transform( - { - "content": content, - "container_tag": container_tag, - "container_tags": container_tags, - "custom_id": custom_id, - "document_date": document_date, - "dreaming": dreaming, - "entity_context": entity_context, - "filepath": filepath, - "filter_by_metadata": filter_by_metadata, - "metadata": metadata, - "task_type": task_type, - }, - document_add_params.DocumentAddParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=DocumentAddResponse, - ) - - async def batch_add( - self, - *, - documents: Union[Iterable[document_batch_add_params.DocumentsUnionMember0], SequenceNotStr[str]], - container_tag: str | Omit = omit, - container_tags: SequenceNotStr[str] | Omit = omit, - content: None | Omit = omit, - document_date: str | Omit = omit, - dreaming: Literal["instant", "dynamic"] | Omit = omit, - entity_context: str | Omit = omit, - filepath: str | Omit = omit, - filter_by_metadata: Dict[str, Union[str, float, bool, SequenceNotStr[str]]] | Omit = omit, - metadata: Dict[str, Union[str, float, bool, SequenceNotStr[str]]] | Omit = omit, - task_type: Literal["memory", "superrag"] | 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, - ) -> DocumentBatchAddResponse: - """Add multiple documents in a single request. - - Each document can have any content - type (text, url, file, etc.) and metadata - - Args: - 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. - - document_date: When this document's content is from, as opposed to when it was uploaded. - Accepts YYYY-MM-DD or a full ISO 8601 timestamp. Memory extraction resolves - relative dates ('yesterday', 'last quarter') against this instead of the - ingestion time, and documents in a batch are processed oldest-first so newer - facts correctly supersede older ones. Set this whenever you backfill historical - content. - - dreaming: Processing mode. "dynamic" (default) groups related documents together so - memories form from coherent, logical units rather than one isolated entry at a - time. "instant" processes each document on its own right away, and bills one - extra operation per document. - - entity_context: Optional entity context for this container tag. Max 1500 characters. Used during - document processing to guide memory extraction. - - filepath: Optional file path for the document (e.g., '/documents/reports/file.pdf'). Used - by supermemoryfs to map documents to filesystem paths. - - filter_by_metadata: Optional metadata filter scoping which existing memories are pulled as context - during ingestion. Scalar values match exactly (AND across keys); array values - match ANY (OR within key). Only memories whose source documents match this - filter are used as context. - - 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. - - task_type: Task type: "memory" (default) for full context layer with SuperRAG built in, - "superrag" for managed RAG as a service. - - 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( - "/v3/documents/batch", - body=await async_maybe_transform( - { - "documents": documents, - "container_tag": container_tag, - "container_tags": container_tags, - "content": content, - "document_date": document_date, - "dreaming": dreaming, - "entity_context": entity_context, - "filepath": filepath, - "filter_by_metadata": filter_by_metadata, - "metadata": metadata, - "task_type": task_type, - }, - document_batch_add_params.DocumentBatchAddParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=DocumentBatchAddResponse, - ) - - async def delete_bulk( - self, - *, - container_tags: SequenceNotStr[str] | Omit = omit, - filepath: str | Omit = omit, - ids: SequenceNotStr[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, - ) -> DocumentDeleteBulkResponse: - """ - Bulk delete documents by IDs or container tags - - Args: - container_tags: Array of container tags - all documents in these containers will be deleted - - filepath: Delete documents matching this filepath. Exact match for full paths, prefix - match if ending with / - - ids: Array of document IDs to delete (max 100 at once) - - 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._delete( - "/v3/documents/bulk", - body=await async_maybe_transform( - { - "container_tags": container_tags, - "filepath": filepath, - "ids": ids, - }, - document_delete_bulk_params.DocumentDeleteBulkParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=DocumentDeleteBulkResponse, - ) - - async def get( - self, - id: str, - *, - # 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, - ) -> DocumentGetResponse: - """ - Get a document by ID - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - return await self._get( - path_template("/v3/documents/{id}", id=id), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=DocumentGetResponse, - ) - - async def list_processing( - self, - *, - container_tags: str | Omit = omit, - limit: Union[str, float] | Omit = omit, - page: Union[str, float] | Omit = omit, - view: Literal["active", "pending", "all"] | 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, - ) -> DocumentListProcessingResponse: - """Get documents that are currently being processed. - - Default `view=active` is the - live in-flight queue. `view=pending` is every unfinished document with no time - cutoff. `view=all` also includes failed documents, paginated. - - Args: - container_tags: Comma-separated container tags to filter by - - limit: Number of items per page. Used with `view=all`. - - page: Page number to fetch. Used with `view=all`. - - view: `active` returns in-flight documents updated in the last 4 hours. `pending` - returns every document that is not done or failed, with no time cutoff. `all` - also includes failed documents, paginated. - - 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._get( - "/v3/documents/processing", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=await async_maybe_transform( - { - "container_tags": container_tags, - "limit": limit, - "page": page, - "view": view, - }, - document_list_processing_params.DocumentListProcessingParams, - ), - ), - cast_to=DocumentListProcessingResponse, - ) - - async def upload_file( - self, - *, - file: FileTypes, - container_tag: str | Omit = omit, - container_tags: str | Omit = omit, - custom_id: str | Omit = omit, - dreaming: Literal["instant", "dynamic"] | Omit = omit, - entity_context: str | Omit = omit, - filepath: str | Omit = omit, - file_type: str | Omit = omit, - filter_by_metadata: str | Omit = omit, - metadata: str | Omit = omit, - mime_type: str | Omit = omit, - task_type: Literal["memory", "superrag"] | Omit = omit, - use_advanced_processing: 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, - ) -> DocumentUploadFileResponse: - """ - Upload a file to be processed - - Args: - file: File to upload and process - - container_tag: Optional container tag (e.g., 'user_123'). Use this for a single tag. - - container_tags: Optional container tags. Can be either a JSON string of an array (e.g., - '["user_123", "project_123"]') or a single string (e.g., 'user_123'). Single - strings will be automatically converted to an array. - - custom_id: Optional custom ID of the document. Max 100 characters, alphanumeric with - hyphens, underscores, and colons only. - - dreaming: Processing mode. "dynamic" (default) groups related documents together so - memories form from coherent, logical units rather than one isolated entry at a - time. "instant" processes each document on its own right away, and bills one - extra operation per document. - - entity_context: Optional entity context for this container tag. Max 1500 characters. Used during - document processing to guide memory extraction. - - filepath: Optional file path for the uploaded file (e.g., '/documents/reports/file.pdf'). - Used by supermemoryfs to map documents to filesystem paths. - - file_type: - Optional file type override to force specific processing behavior. Valid values: - text, pdf, tweet, google_doc, google_slide, google_sheet, image, video, - notion_doc, webpage, onedrive - - filter_by_metadata: Optional metadata filter as a JSON string. Scopes which existing memories are - pulled as context during ingestion. Scalar values match exactly (AND across - keys); array values match ANY (OR within key). - - metadata: Optional metadata for the document as a JSON string. This is used to store - additional information about the document. Keys must be strings and values can - be strings, numbers, or booleans. - - mime_type: Required when fileType is 'image' or 'video'. Specifies the exact MIME type to - use (e.g., 'image/png', 'image/jpeg', 'video/mp4', 'video/webm') - - task_type: Task type: "memory" (default) for full context layer with SuperRAG built in, - "superrag" for managed RAG as a service. - - use_advanced_processing: DEPRECATED: This field is no longer used. Advanced PDF processing is now - automatic with our hybrid Mistral OCR + Gemini pipeline. This parameter will be - accepted but ignored for backwards compatibility. - - 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_with_paths( - { - "file": file, - "container_tag": container_tag, - "container_tags": container_tags, - "custom_id": custom_id, - "dreaming": dreaming, - "entity_context": entity_context, - "filepath": filepath, - "file_type": file_type, - "filter_by_metadata": filter_by_metadata, - "metadata": metadata, - "mime_type": mime_type, - "task_type": task_type, - "use_advanced_processing": use_advanced_processing, - }, - [["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/documents/file", - body=await async_maybe_transform(body, document_upload_file_params.DocumentUploadFileParams), - files=files, - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=DocumentUploadFileResponse, - ) - - -class DocumentsResourceWithRawResponse: - def __init__(self, documents: DocumentsResource) -> None: - self._documents = documents - - self.update = to_raw_response_wrapper( - documents.update, - ) - self.list = to_raw_response_wrapper( - documents.list, - ) - self.delete = to_raw_response_wrapper( - documents.delete, - ) - self.add = to_raw_response_wrapper( - documents.add, - ) - self.batch_add = to_raw_response_wrapper( - documents.batch_add, - ) - self.delete_bulk = to_raw_response_wrapper( - documents.delete_bulk, - ) - self.get = to_raw_response_wrapper( - documents.get, - ) - self.list_processing = to_raw_response_wrapper( - documents.list_processing, - ) - self.upload_file = to_raw_response_wrapper( - documents.upload_file, - ) - - -class AsyncDocumentsResourceWithRawResponse: - def __init__(self, documents: AsyncDocumentsResource) -> None: - self._documents = documents - - self.update = async_to_raw_response_wrapper( - documents.update, - ) - self.list = async_to_raw_response_wrapper( - documents.list, - ) - self.delete = async_to_raw_response_wrapper( - documents.delete, - ) - self.add = async_to_raw_response_wrapper( - documents.add, - ) - self.batch_add = async_to_raw_response_wrapper( - documents.batch_add, - ) - self.delete_bulk = async_to_raw_response_wrapper( - documents.delete_bulk, - ) - self.get = async_to_raw_response_wrapper( - documents.get, - ) - self.list_processing = async_to_raw_response_wrapper( - documents.list_processing, - ) - self.upload_file = async_to_raw_response_wrapper( - documents.upload_file, - ) - - -class DocumentsResourceWithStreamingResponse: - def __init__(self, documents: DocumentsResource) -> None: - self._documents = documents - - self.update = to_streamed_response_wrapper( - documents.update, - ) - self.list = to_streamed_response_wrapper( - documents.list, - ) - self.delete = to_streamed_response_wrapper( - documents.delete, - ) - self.add = to_streamed_response_wrapper( - documents.add, - ) - self.batch_add = to_streamed_response_wrapper( - documents.batch_add, - ) - self.delete_bulk = to_streamed_response_wrapper( - documents.delete_bulk, - ) - self.get = to_streamed_response_wrapper( - documents.get, - ) - self.list_processing = to_streamed_response_wrapper( - documents.list_processing, - ) - self.upload_file = to_streamed_response_wrapper( - documents.upload_file, - ) - - -class AsyncDocumentsResourceWithStreamingResponse: - def __init__(self, documents: AsyncDocumentsResource) -> None: - self._documents = documents - - self.update = async_to_streamed_response_wrapper( - documents.update, - ) - self.list = async_to_streamed_response_wrapper( - documents.list, - ) - self.delete = async_to_streamed_response_wrapper( - documents.delete, - ) - self.add = async_to_streamed_response_wrapper( - documents.add, - ) - self.batch_add = async_to_streamed_response_wrapper( - documents.batch_add, - ) - self.delete_bulk = async_to_streamed_response_wrapper( - documents.delete_bulk, - ) - self.get = async_to_streamed_response_wrapper( - documents.get, - ) - self.list_processing = async_to_streamed_response_wrapper( - documents.list_processing, - ) - self.upload_file = async_to_streamed_response_wrapper( - documents.upload_file, - ) diff --git a/src/supermemory/resources/memories.py b/src/supermemory/resources/memories.py deleted file mode 100644 index e701928e..00000000 --- a/src/supermemory/resources/memories.py +++ /dev/null @@ -1,374 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Dict, Union, Optional - -import httpx - -from ..types import memory_forget_params, memory_update_memory_params -from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, 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.memory_forget_response import MemoryForgetResponse -from ..types.memory_update_memory_response import MemoryUpdateMemoryResponse - -__all__ = ["MemoriesResource", "AsyncMemoriesResource"] - - -class MemoriesResource(SyncAPIResource): - """List, get, update, and delete content and memories""" - - @cached_property - def with_raw_response(self) -> MemoriesResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/supermemoryai/python-sdk#accessing-raw-response-data-eg-headers - """ - return MemoriesResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> MemoriesResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/supermemoryai/python-sdk#with_streaming_response - """ - return MemoriesResourceWithStreamingResponse(self) - - def forget( - self, - *, - container_tag: str, - id: str | Omit = omit, - content: str | Omit = omit, - reason: 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, - ) -> MemoryForgetResponse: - """Forget (soft delete) a memory entry. - - The memory is marked as forgotten but not - permanently deleted. - - Args: - container_tag: Container tag / space identifier. Required to scope the operation. - - id: ID of the memory entry to operate on - - content: Exact content match of the memory entry to operate on. Use this when you don't - have the ID. - - reason: Optional reason for forgetting this memory - - 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._delete( - "/v4/memories", - body=maybe_transform( - { - "container_tag": container_tag, - "id": id, - "content": content, - "reason": reason, - }, - memory_forget_params.MemoryForgetParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=MemoryForgetResponse, - ) - - def update_memory( - self, - *, - container_tag: str, - new_content: str, - id: str | Omit = omit, - content: str | Omit = omit, - forget_after: Optional[str] | Omit = omit, - forget_reason: Optional[str] | Omit = omit, - metadata: Dict[str, Union[str, float, bool, SequenceNotStr[str]]] | Omit = omit, - temporal_context: memory_update_memory_params.TemporalContext | 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, - ) -> MemoryUpdateMemoryResponse: - """Update a memory by creating a new version. - - The original memory is preserved with - isLatest=false. - - Args: - container_tag: Container tag / space identifier. Required to scope the operation. - - new_content: The new content that will replace the existing memory - - id: ID of the memory entry to operate on - - content: Exact content match of the memory entry to operate on. Use this when you don't - have the ID. - - forget_after: ISO 8601 datetime string. The memory will be auto-forgotten after this time. - Pass null to clear an existing expiry. Omit to inherit from the previous - version. - - forget_reason: Optional reason for the scheduled forgetting. Cleared automatically when - forgetAfter is set to null. - - metadata: Optional metadata. If not provided, inherits from the previous version. - - temporal_context: Structured temporal metadata. Merged into the metadata JSON column. If omitted, - existing temporalContext is preserved. - - 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._patch( - "/v4/memories", - body=maybe_transform( - { - "container_tag": container_tag, - "new_content": new_content, - "id": id, - "content": content, - "forget_after": forget_after, - "forget_reason": forget_reason, - "metadata": metadata, - "temporal_context": temporal_context, - }, - memory_update_memory_params.MemoryUpdateMemoryParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=MemoryUpdateMemoryResponse, - ) - - -class AsyncMemoriesResource(AsyncAPIResource): - """List, get, update, and delete content and memories""" - - @cached_property - def with_raw_response(self) -> AsyncMemoriesResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/supermemoryai/python-sdk#accessing-raw-response-data-eg-headers - """ - return AsyncMemoriesResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncMemoriesResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/supermemoryai/python-sdk#with_streaming_response - """ - return AsyncMemoriesResourceWithStreamingResponse(self) - - async def forget( - self, - *, - container_tag: str, - id: str | Omit = omit, - content: str | Omit = omit, - reason: 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, - ) -> MemoryForgetResponse: - """Forget (soft delete) a memory entry. - - The memory is marked as forgotten but not - permanently deleted. - - Args: - container_tag: Container tag / space identifier. Required to scope the operation. - - id: ID of the memory entry to operate on - - content: Exact content match of the memory entry to operate on. Use this when you don't - have the ID. - - reason: Optional reason for forgetting this memory - - 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._delete( - "/v4/memories", - body=await async_maybe_transform( - { - "container_tag": container_tag, - "id": id, - "content": content, - "reason": reason, - }, - memory_forget_params.MemoryForgetParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=MemoryForgetResponse, - ) - - async def update_memory( - self, - *, - container_tag: str, - new_content: str, - id: str | Omit = omit, - content: str | Omit = omit, - forget_after: Optional[str] | Omit = omit, - forget_reason: Optional[str] | Omit = omit, - metadata: Dict[str, Union[str, float, bool, SequenceNotStr[str]]] | Omit = omit, - temporal_context: memory_update_memory_params.TemporalContext | 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, - ) -> MemoryUpdateMemoryResponse: - """Update a memory by creating a new version. - - The original memory is preserved with - isLatest=false. - - Args: - container_tag: Container tag / space identifier. Required to scope the operation. - - new_content: The new content that will replace the existing memory - - id: ID of the memory entry to operate on - - content: Exact content match of the memory entry to operate on. Use this when you don't - have the ID. - - forget_after: ISO 8601 datetime string. The memory will be auto-forgotten after this time. - Pass null to clear an existing expiry. Omit to inherit from the previous - version. - - forget_reason: Optional reason for the scheduled forgetting. Cleared automatically when - forgetAfter is set to null. - - metadata: Optional metadata. If not provided, inherits from the previous version. - - temporal_context: Structured temporal metadata. Merged into the metadata JSON column. If omitted, - existing temporalContext is preserved. - - 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._patch( - "/v4/memories", - body=await async_maybe_transform( - { - "container_tag": container_tag, - "new_content": new_content, - "id": id, - "content": content, - "forget_after": forget_after, - "forget_reason": forget_reason, - "metadata": metadata, - "temporal_context": temporal_context, - }, - memory_update_memory_params.MemoryUpdateMemoryParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=MemoryUpdateMemoryResponse, - ) - - -class MemoriesResourceWithRawResponse: - def __init__(self, memories: MemoriesResource) -> None: - self._memories = memories - - self.forget = to_raw_response_wrapper( - memories.forget, - ) - self.update_memory = to_raw_response_wrapper( - memories.update_memory, - ) - - -class AsyncMemoriesResourceWithRawResponse: - def __init__(self, memories: AsyncMemoriesResource) -> None: - self._memories = memories - - self.forget = async_to_raw_response_wrapper( - memories.forget, - ) - self.update_memory = async_to_raw_response_wrapper( - memories.update_memory, - ) - - -class MemoriesResourceWithStreamingResponse: - def __init__(self, memories: MemoriesResource) -> None: - self._memories = memories - - self.forget = to_streamed_response_wrapper( - memories.forget, - ) - self.update_memory = to_streamed_response_wrapper( - memories.update_memory, - ) - - -class AsyncMemoriesResourceWithStreamingResponse: - def __init__(self, memories: AsyncMemoriesResource) -> None: - self._memories = memories - - self.forget = async_to_streamed_response_wrapper( - memories.forget, - ) - self.update_memory = async_to_streamed_response_wrapper( - memories.update_memory, - ) diff --git a/src/supermemory/resources/search.py b/src/supermemory/resources/search.py deleted file mode 100644 index 3b092bb5..00000000 --- a/src/supermemory/resources/search.py +++ /dev/null @@ -1,739 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Literal - -import httpx - -from ..types import search_execute_params, search_memories_params, search_documents_params -from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, 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.search_execute_response import SearchExecuteResponse -from ..types.search_memories_response import SearchMemoriesResponse -from ..types.search_documents_response import SearchDocumentsResponse - -__all__ = ["SearchResource", "AsyncSearchResource"] - - -class SearchResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> SearchResourceWithRawResponse: - """ - 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 SearchResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> SearchResourceWithStreamingResponse: - """ - 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 SearchResourceWithStreamingResponse(self) - - def documents( - self, - *, - q: str, - categories_filter: SequenceNotStr[str] | Omit = omit, - chunk_threshold: float | Omit = omit, - container_tag: str | Omit = omit, - container_tags: SequenceNotStr[str] | Omit = omit, - doc_id: str | Omit = omit, - document_threshold: float | Omit = omit, - filepath: str | Omit = omit, - filters: search_documents_params.Filters | Omit = omit, - include_full_docs: bool | Omit = omit, - include_summary: bool | Omit = omit, - limit: int | Omit = omit, - only_matching_chunks: bool | Omit = omit, - rerank: bool | Omit = omit, - rewrite_query: bool | 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, - ) -> SearchDocumentsResponse: - """ - Search memories with advanced filtering - - Args: - q: Search query string - - 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 - results) - - container_tag: Optional single container tag. Use this or containerTags. - - container_tags: Optional tags this search should be containerized by. This can be an ID for your - user, a project ID, or any other identifier you wish to use to filter documents. - - doc_id: Optional document ID to search within. You can use this to find chunks in a very - large document. - - document_threshold: DEPRECATED: This field is no longer used in v3 search. The search now uses - chunkThreshold only. This parameter will be ignored. - - filepath: Filter search results by filepath. Exact match for full paths, prefix match if - ending with / - - filters: Optional filters to apply to the search. Can be a JSON string or Query object. - - include_full_docs: If true, include full document in the response. This is helpful if you want a - chatbot to know the full context of the document. - - include_summary: If true, include document summary in the response. This is helpful if you want a - chatbot to know the full context of the document. - - limit: Maximum number of results to return - - only_matching_chunks: If true, only return matching chunks without context. Normally, we send the - previous and next chunk to provide more context for LLMs. If you only want the - matching chunk, set this to true. - - rerank: If true, rerank the results based on the query. This is helpful if you want to - ensure the most relevant results are returned. - - rewrite_query: If true, rewrites the query to make it easier to find documents. This increases - the latency by about 400ms - - 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( - "/v3/search", - body=maybe_transform( - { - "q": q, - "categories_filter": categories_filter, - "chunk_threshold": chunk_threshold, - "container_tag": container_tag, - "container_tags": container_tags, - "doc_id": doc_id, - "document_threshold": document_threshold, - "filepath": filepath, - "filters": filters, - "include_full_docs": include_full_docs, - "include_summary": include_summary, - "limit": limit, - "only_matching_chunks": only_matching_chunks, - "rerank": rerank, - "rewrite_query": rewrite_query, - }, - search_documents_params.SearchDocumentsParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SearchDocumentsResponse, - ) - - def execute( - self, - *, - q: str, - categories_filter: SequenceNotStr[str] | Omit = omit, - chunk_threshold: float | Omit = omit, - container_tag: str | Omit = omit, - container_tags: SequenceNotStr[str] | Omit = omit, - doc_id: str | Omit = omit, - document_threshold: float | Omit = omit, - filepath: str | Omit = omit, - filters: search_execute_params.Filters | Omit = omit, - include_full_docs: bool | Omit = omit, - include_summary: bool | Omit = omit, - limit: int | Omit = omit, - only_matching_chunks: bool | Omit = omit, - rerank: bool | Omit = omit, - rewrite_query: bool | 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, - ) -> SearchExecuteResponse: - """ - Search memories with advanced filtering - - Args: - q: Search query string - - 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 - results) - - container_tag: Optional single container tag. Use this or containerTags. - - container_tags: Optional tags this search should be containerized by. This can be an ID for your - user, a project ID, or any other identifier you wish to use to filter documents. - - doc_id: Optional document ID to search within. You can use this to find chunks in a very - large document. - - document_threshold: DEPRECATED: This field is no longer used in v3 search. The search now uses - chunkThreshold only. This parameter will be ignored. - - filepath: Filter search results by filepath. Exact match for full paths, prefix match if - ending with / - - filters: Optional filters to apply to the search. Can be a JSON string or Query object. - - include_full_docs: If true, include full document in the response. This is helpful if you want a - chatbot to know the full context of the document. - - include_summary: If true, include document summary in the response. This is helpful if you want a - chatbot to know the full context of the document. - - limit: Maximum number of results to return - - only_matching_chunks: If true, only return matching chunks without context. Normally, we send the - previous and next chunk to provide more context for LLMs. If you only want the - matching chunk, set this to true. - - rerank: If true, rerank the results based on the query. This is helpful if you want to - ensure the most relevant results are returned. - - rewrite_query: If true, rewrites the query to make it easier to find documents. This increases - the latency by about 400ms - - 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( - "/v3/search", - body=maybe_transform( - { - "q": q, - "categories_filter": categories_filter, - "chunk_threshold": chunk_threshold, - "container_tag": container_tag, - "container_tags": container_tags, - "doc_id": doc_id, - "document_threshold": document_threshold, - "filepath": filepath, - "filters": filters, - "include_full_docs": include_full_docs, - "include_summary": include_summary, - "limit": limit, - "only_matching_chunks": only_matching_chunks, - "rerank": rerank, - "rewrite_query": rewrite_query, - }, - search_execute_params.SearchExecuteParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SearchExecuteResponse, - ) - - def memories( - self, - *, - q: str, - aggregate: bool | Omit = omit, - container_tag: str | Omit = omit, - container_tags: SequenceNotStr[str] | Omit = omit, - filepath: str | Omit = omit, - filters: search_memories_params.Filters | Omit = omit, - include: search_memories_params.Include | Omit = omit, - limit: int | Omit = omit, - rerank: bool | Omit = omit, - rewrite_query: bool | Omit = omit, - search_mode: Literal["memories", "hybrid", "documents"] | Omit = omit, - threshold: float | 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, - ) -> SearchMemoriesResponse: - """ - Search memory entries - Low latency for conversational - - Args: - q: Search query string - - aggregate: If true, aggregates information from multiple memories to create new synthesized - memories. The result will be a mix of aggregated and non-aggregated memories, - reranked by relevance to the query. Works in conjunction with reranking. - - container_tag: Optional tag this search should be containerized by. This can be an ID for your - user, a project ID, or any other identifier you wish to use to filter memories. - - container_tags: Optional tags this search should be containerized by. Search is scoped to - memories under these tags. - - filepath: Filter search results by filepath. Exact match for full paths, prefix match if - ending with / - - filters: Optional filters to apply to the search. Can be a JSON string or Query object. - - limit: Maximum number of results to return - - rerank: If true, rerank the results based on the query. This is helpful if you want to - ensure the most relevant results are returned. - - rewrite_query: If true, rewrites the query to make it easier to find documents. This increases - the latency by about 400ms - - search_mode: Search mode. 'memories' searches only memory entries (default). 'hybrid' - searches both memories and document chunks. 'documents' searches only document - chunks. - - threshold: Threshold / sensitivity for memories selection. 0 is least sensitive (returns - most memories, more results), 1 is most sensitive (returns lesser memories, - accurate results) - - 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/search", - body=maybe_transform( - { - "q": q, - "aggregate": aggregate, - "container_tag": container_tag, - "container_tags": container_tags, - "filepath": filepath, - "filters": filters, - "include": include, - "limit": limit, - "rerank": rerank, - "rewrite_query": rewrite_query, - "search_mode": search_mode, - "threshold": threshold, - }, - search_memories_params.SearchMemoriesParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SearchMemoriesResponse, - ) - - -class AsyncSearchResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncSearchResourceWithRawResponse: - """ - 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 AsyncSearchResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncSearchResourceWithStreamingResponse: - """ - 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 AsyncSearchResourceWithStreamingResponse(self) - - async def documents( - self, - *, - q: str, - categories_filter: SequenceNotStr[str] | Omit = omit, - chunk_threshold: float | Omit = omit, - container_tag: str | Omit = omit, - container_tags: SequenceNotStr[str] | Omit = omit, - doc_id: str | Omit = omit, - document_threshold: float | Omit = omit, - filepath: str | Omit = omit, - filters: search_documents_params.Filters | Omit = omit, - include_full_docs: bool | Omit = omit, - include_summary: bool | Omit = omit, - limit: int | Omit = omit, - only_matching_chunks: bool | Omit = omit, - rerank: bool | Omit = omit, - rewrite_query: bool | 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, - ) -> SearchDocumentsResponse: - """ - Search memories with advanced filtering - - Args: - q: Search query string - - 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 - results) - - container_tag: Optional single container tag. Use this or containerTags. - - container_tags: Optional tags this search should be containerized by. This can be an ID for your - user, a project ID, or any other identifier you wish to use to filter documents. - - doc_id: Optional document ID to search within. You can use this to find chunks in a very - large document. - - document_threshold: DEPRECATED: This field is no longer used in v3 search. The search now uses - chunkThreshold only. This parameter will be ignored. - - filepath: Filter search results by filepath. Exact match for full paths, prefix match if - ending with / - - filters: Optional filters to apply to the search. Can be a JSON string or Query object. - - include_full_docs: If true, include full document in the response. This is helpful if you want a - chatbot to know the full context of the document. - - include_summary: If true, include document summary in the response. This is helpful if you want a - chatbot to know the full context of the document. - - limit: Maximum number of results to return - - only_matching_chunks: If true, only return matching chunks without context. Normally, we send the - previous and next chunk to provide more context for LLMs. If you only want the - matching chunk, set this to true. - - rerank: If true, rerank the results based on the query. This is helpful if you want to - ensure the most relevant results are returned. - - rewrite_query: If true, rewrites the query to make it easier to find documents. This increases - the latency by about 400ms - - 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( - "/v3/search", - body=await async_maybe_transform( - { - "q": q, - "categories_filter": categories_filter, - "chunk_threshold": chunk_threshold, - "container_tag": container_tag, - "container_tags": container_tags, - "doc_id": doc_id, - "document_threshold": document_threshold, - "filepath": filepath, - "filters": filters, - "include_full_docs": include_full_docs, - "include_summary": include_summary, - "limit": limit, - "only_matching_chunks": only_matching_chunks, - "rerank": rerank, - "rewrite_query": rewrite_query, - }, - search_documents_params.SearchDocumentsParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SearchDocumentsResponse, - ) - - async def execute( - self, - *, - q: str, - categories_filter: SequenceNotStr[str] | Omit = omit, - chunk_threshold: float | Omit = omit, - container_tag: str | Omit = omit, - container_tags: SequenceNotStr[str] | Omit = omit, - doc_id: str | Omit = omit, - document_threshold: float | Omit = omit, - filepath: str | Omit = omit, - filters: search_execute_params.Filters | Omit = omit, - include_full_docs: bool | Omit = omit, - include_summary: bool | Omit = omit, - limit: int | Omit = omit, - only_matching_chunks: bool | Omit = omit, - rerank: bool | Omit = omit, - rewrite_query: bool | 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, - ) -> SearchExecuteResponse: - """ - Search memories with advanced filtering - - Args: - q: Search query string - - 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 - results) - - container_tag: Optional single container tag. Use this or containerTags. - - container_tags: Optional tags this search should be containerized by. This can be an ID for your - user, a project ID, or any other identifier you wish to use to filter documents. - - doc_id: Optional document ID to search within. You can use this to find chunks in a very - large document. - - document_threshold: DEPRECATED: This field is no longer used in v3 search. The search now uses - chunkThreshold only. This parameter will be ignored. - - filepath: Filter search results by filepath. Exact match for full paths, prefix match if - ending with / - - filters: Optional filters to apply to the search. Can be a JSON string or Query object. - - include_full_docs: If true, include full document in the response. This is helpful if you want a - chatbot to know the full context of the document. - - include_summary: If true, include document summary in the response. This is helpful if you want a - chatbot to know the full context of the document. - - limit: Maximum number of results to return - - only_matching_chunks: If true, only return matching chunks without context. Normally, we send the - previous and next chunk to provide more context for LLMs. If you only want the - matching chunk, set this to true. - - rerank: If true, rerank the results based on the query. This is helpful if you want to - ensure the most relevant results are returned. - - rewrite_query: If true, rewrites the query to make it easier to find documents. This increases - the latency by about 400ms - - 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( - "/v3/search", - body=await async_maybe_transform( - { - "q": q, - "categories_filter": categories_filter, - "chunk_threshold": chunk_threshold, - "container_tag": container_tag, - "container_tags": container_tags, - "doc_id": doc_id, - "document_threshold": document_threshold, - "filepath": filepath, - "filters": filters, - "include_full_docs": include_full_docs, - "include_summary": include_summary, - "limit": limit, - "only_matching_chunks": only_matching_chunks, - "rerank": rerank, - "rewrite_query": rewrite_query, - }, - search_execute_params.SearchExecuteParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SearchExecuteResponse, - ) - - async def memories( - self, - *, - q: str, - aggregate: bool | Omit = omit, - container_tag: str | Omit = omit, - container_tags: SequenceNotStr[str] | Omit = omit, - filepath: str | Omit = omit, - filters: search_memories_params.Filters | Omit = omit, - include: search_memories_params.Include | Omit = omit, - limit: int | Omit = omit, - rerank: bool | Omit = omit, - rewrite_query: bool | Omit = omit, - search_mode: Literal["memories", "hybrid", "documents"] | Omit = omit, - threshold: float | 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, - ) -> SearchMemoriesResponse: - """ - Search memory entries - Low latency for conversational - - Args: - q: Search query string - - aggregate: If true, aggregates information from multiple memories to create new synthesized - memories. The result will be a mix of aggregated and non-aggregated memories, - reranked by relevance to the query. Works in conjunction with reranking. - - container_tag: Optional tag this search should be containerized by. This can be an ID for your - user, a project ID, or any other identifier you wish to use to filter memories. - - container_tags: Optional tags this search should be containerized by. Search is scoped to - memories under these tags. - - filepath: Filter search results by filepath. Exact match for full paths, prefix match if - ending with / - - filters: Optional filters to apply to the search. Can be a JSON string or Query object. - - limit: Maximum number of results to return - - rerank: If true, rerank the results based on the query. This is helpful if you want to - ensure the most relevant results are returned. - - rewrite_query: If true, rewrites the query to make it easier to find documents. This increases - the latency by about 400ms - - search_mode: Search mode. 'memories' searches only memory entries (default). 'hybrid' - searches both memories and document chunks. 'documents' searches only document - chunks. - - threshold: Threshold / sensitivity for memories selection. 0 is least sensitive (returns - most memories, more results), 1 is most sensitive (returns lesser memories, - accurate results) - - 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/search", - body=await async_maybe_transform( - { - "q": q, - "aggregate": aggregate, - "container_tag": container_tag, - "container_tags": container_tags, - "filepath": filepath, - "filters": filters, - "include": include, - "limit": limit, - "rerank": rerank, - "rewrite_query": rewrite_query, - "search_mode": search_mode, - "threshold": threshold, - }, - search_memories_params.SearchMemoriesParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SearchMemoriesResponse, - ) - - -class SearchResourceWithRawResponse: - def __init__(self, search: SearchResource) -> None: - self._search = search - - self.documents = to_raw_response_wrapper( - search.documents, - ) - self.execute = to_raw_response_wrapper( - search.execute, - ) - self.memories = to_raw_response_wrapper( - search.memories, - ) - - -class AsyncSearchResourceWithRawResponse: - def __init__(self, search: AsyncSearchResource) -> None: - self._search = search - - self.documents = async_to_raw_response_wrapper( - search.documents, - ) - self.execute = async_to_raw_response_wrapper( - search.execute, - ) - self.memories = async_to_raw_response_wrapper( - search.memories, - ) - - -class SearchResourceWithStreamingResponse: - def __init__(self, search: SearchResource) -> None: - self._search = search - - self.documents = to_streamed_response_wrapper( - search.documents, - ) - self.execute = to_streamed_response_wrapper( - search.execute, - ) - self.memories = to_streamed_response_wrapper( - search.memories, - ) - - -class AsyncSearchResourceWithStreamingResponse: - def __init__(self, search: AsyncSearchResource) -> None: - self._search = search - - self.documents = async_to_streamed_response_wrapper( - search.documents, - ) - self.execute = async_to_streamed_response_wrapper( - search.execute, - ) - self.memories = async_to_streamed_response_wrapper( - search.memories, - ) diff --git a/src/supermemory/resources/settings.py b/src/supermemory/resources/settings.py deleted file mode 100644 index d191459b..00000000 --- a/src/supermemory/resources/settings.py +++ /dev/null @@ -1,302 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Dict, Union, Iterable, Optional - -import httpx - -from ..types import setting_update_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.setting_get_response import SettingGetResponse -from ..types.setting_update_response import SettingUpdateResponse - -__all__ = ["SettingsResource", "AsyncSettingsResource"] - - -class SettingsResource(SyncAPIResource): - """Organization settings""" - - @cached_property - def with_raw_response(self) -> SettingsResourceWithRawResponse: - """ - 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 SettingsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> SettingsResourceWithStreamingResponse: - """ - 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 SettingsResourceWithStreamingResponse(self) - - def update( - self, - *, - chunk_size: Optional[int] | Omit = omit, - exclude_items: Union[str, float, bool, Dict[str, object], Iterable[object], None] | Omit = omit, - filter_prompt: Optional[str] | Omit = omit, - github_client_id: Optional[str] | Omit = omit, - github_client_secret: Optional[str] | Omit = omit, - github_custom_key_enabled: Optional[bool] | Omit = omit, - google_drive_client_id: Optional[str] | Omit = omit, - google_drive_client_secret: Optional[str] | Omit = omit, - google_drive_custom_key_enabled: Optional[bool] | Omit = omit, - include_items: Union[str, float, bool, Dict[str, object], Iterable[object], None] | Omit = omit, - notion_client_id: Optional[str] | Omit = omit, - notion_client_secret: Optional[str] | Omit = omit, - notion_custom_key_enabled: Optional[bool] | Omit = omit, - onedrive_client_id: Optional[str] | Omit = omit, - onedrive_client_secret: Optional[str] | Omit = omit, - onedrive_custom_key_enabled: Optional[bool] | Omit = omit, - profile_buckets: Iterable[setting_update_params.ProfileBucket] | Omit = omit, - should_llm_filter: Optional[bool] | 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, - ) -> SettingUpdateResponse: - """ - Update settings for an organization - - Args: - profile_buckets: Profile bucket definitions - - 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._patch( - "/v3/settings", - body=maybe_transform( - { - "chunk_size": chunk_size, - "exclude_items": exclude_items, - "filter_prompt": filter_prompt, - "github_client_id": github_client_id, - "github_client_secret": github_client_secret, - "github_custom_key_enabled": github_custom_key_enabled, - "google_drive_client_id": google_drive_client_id, - "google_drive_client_secret": google_drive_client_secret, - "google_drive_custom_key_enabled": google_drive_custom_key_enabled, - "include_items": include_items, - "notion_client_id": notion_client_id, - "notion_client_secret": notion_client_secret, - "notion_custom_key_enabled": notion_custom_key_enabled, - "onedrive_client_id": onedrive_client_id, - "onedrive_client_secret": onedrive_client_secret, - "onedrive_custom_key_enabled": onedrive_custom_key_enabled, - "profile_buckets": profile_buckets, - "should_llm_filter": should_llm_filter, - }, - setting_update_params.SettingUpdateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SettingUpdateResponse, - ) - - def get( - self, - *, - # 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, - ) -> SettingGetResponse: - """Get settings for an organization""" - return self._get( - "/v3/settings", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SettingGetResponse, - ) - - -class AsyncSettingsResource(AsyncAPIResource): - """Organization settings""" - - @cached_property - def with_raw_response(self) -> AsyncSettingsResourceWithRawResponse: - """ - 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 AsyncSettingsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncSettingsResourceWithStreamingResponse: - """ - 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 AsyncSettingsResourceWithStreamingResponse(self) - - async def update( - self, - *, - chunk_size: Optional[int] | Omit = omit, - exclude_items: Union[str, float, bool, Dict[str, object], Iterable[object], None] | Omit = omit, - filter_prompt: Optional[str] | Omit = omit, - github_client_id: Optional[str] | Omit = omit, - github_client_secret: Optional[str] | Omit = omit, - github_custom_key_enabled: Optional[bool] | Omit = omit, - google_drive_client_id: Optional[str] | Omit = omit, - google_drive_client_secret: Optional[str] | Omit = omit, - google_drive_custom_key_enabled: Optional[bool] | Omit = omit, - include_items: Union[str, float, bool, Dict[str, object], Iterable[object], None] | Omit = omit, - notion_client_id: Optional[str] | Omit = omit, - notion_client_secret: Optional[str] | Omit = omit, - notion_custom_key_enabled: Optional[bool] | Omit = omit, - onedrive_client_id: Optional[str] | Omit = omit, - onedrive_client_secret: Optional[str] | Omit = omit, - onedrive_custom_key_enabled: Optional[bool] | Omit = omit, - profile_buckets: Iterable[setting_update_params.ProfileBucket] | Omit = omit, - should_llm_filter: Optional[bool] | 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, - ) -> SettingUpdateResponse: - """ - Update settings for an organization - - Args: - profile_buckets: Profile bucket definitions - - 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._patch( - "/v3/settings", - body=await async_maybe_transform( - { - "chunk_size": chunk_size, - "exclude_items": exclude_items, - "filter_prompt": filter_prompt, - "github_client_id": github_client_id, - "github_client_secret": github_client_secret, - "github_custom_key_enabled": github_custom_key_enabled, - "google_drive_client_id": google_drive_client_id, - "google_drive_client_secret": google_drive_client_secret, - "google_drive_custom_key_enabled": google_drive_custom_key_enabled, - "include_items": include_items, - "notion_client_id": notion_client_id, - "notion_client_secret": notion_client_secret, - "notion_custom_key_enabled": notion_custom_key_enabled, - "onedrive_client_id": onedrive_client_id, - "onedrive_client_secret": onedrive_client_secret, - "onedrive_custom_key_enabled": onedrive_custom_key_enabled, - "profile_buckets": profile_buckets, - "should_llm_filter": should_llm_filter, - }, - setting_update_params.SettingUpdateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SettingUpdateResponse, - ) - - async def get( - self, - *, - # 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, - ) -> SettingGetResponse: - """Get settings for an organization""" - return await self._get( - "/v3/settings", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=SettingGetResponse, - ) - - -class SettingsResourceWithRawResponse: - def __init__(self, settings: SettingsResource) -> None: - self._settings = settings - - self.update = to_raw_response_wrapper( - settings.update, - ) - self.get = to_raw_response_wrapper( - settings.get, - ) - - -class AsyncSettingsResourceWithRawResponse: - def __init__(self, settings: AsyncSettingsResource) -> None: - self._settings = settings - - self.update = async_to_raw_response_wrapper( - settings.update, - ) - self.get = async_to_raw_response_wrapper( - settings.get, - ) - - -class SettingsResourceWithStreamingResponse: - def __init__(self, settings: SettingsResource) -> None: - self._settings = settings - - self.update = to_streamed_response_wrapper( - settings.update, - ) - self.get = to_streamed_response_wrapper( - settings.get, - ) - - -class AsyncSettingsResourceWithStreamingResponse: - def __init__(self, settings: AsyncSettingsResource) -> None: - self._settings = settings - - self.update = async_to_streamed_response_wrapper( - settings.update, - ) - self.get = async_to_streamed_response_wrapper( - settings.get, - ) diff --git a/src/supermemory/types/__init__.py b/src/supermemory/types/__init__.py index 9fcde7c1..6c33597e 100644 --- a/src/supermemory/types/__init__.py +++ b/src/supermemory/types/__init__.py @@ -1,57 +1,370 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from .add_response import AddResponse as AddResponse -from .profile_response import ProfileResponse as ProfileResponse -from .client_add_params import ClientAddParams as ClientAddParams -from .document_add_params import DocumentAddParams as DocumentAddParams -from .document_list_params import DocumentListParams as DocumentListParams -from .memory_forget_params import MemoryForgetParams as MemoryForgetParams -from .setting_get_response import SettingGetResponse as SettingGetResponse -from .client_profile_params import ClientProfileParams as ClientProfileParams -from .document_add_response import DocumentAddResponse as DocumentAddResponse -from .document_get_response import DocumentGetResponse as DocumentGetResponse -from .search_execute_params import SearchExecuteParams as SearchExecuteParams -from .setting_update_params import SettingUpdateParams as SettingUpdateParams -from .connection_list_params import ConnectionListParams as ConnectionListParams -from .document_list_response import DocumentListResponse as DocumentListResponse -from .document_update_params import DocumentUpdateParams as DocumentUpdateParams -from .memory_forget_response import MemoryForgetResponse as MemoryForgetResponse -from .search_memories_params import SearchMemoriesParams as SearchMemoriesParams -from .search_documents_params import SearchDocumentsParams as SearchDocumentsParams -from .search_execute_response import SearchExecuteResponse as SearchExecuteResponse -from .setting_update_response import SettingUpdateResponse as SettingUpdateResponse -from .connection_create_params import ConnectionCreateParams as ConnectionCreateParams -from .connection_import_params import ConnectionImportParams as ConnectionImportParams -from .connection_list_response import ConnectionListResponse as ConnectionListResponse -from .document_update_response import DocumentUpdateResponse as DocumentUpdateResponse -from .search_memories_response import SearchMemoriesResponse as SearchMemoriesResponse -from .document_batch_add_params import DocumentBatchAddParams as DocumentBatchAddParams -from .search_documents_response import SearchDocumentsResponse as SearchDocumentsResponse -from .connection_create_response import ConnectionCreateResponse as ConnectionCreateResponse -from .connection_import_response import ConnectionImportResponse as ConnectionImportResponse -from .connection_configure_params import ConnectionConfigureParams as ConnectionConfigureParams -from .connection_resources_params import ConnectionResourcesParams as ConnectionResourcesParams -from .document_batch_add_response import DocumentBatchAddResponse as DocumentBatchAddResponse -from .document_delete_bulk_params import DocumentDeleteBulkParams as DocumentDeleteBulkParams -from .document_upload_file_params import DocumentUploadFileParams as DocumentUploadFileParams -from .memory_update_memory_params import MemoryUpdateMemoryParams as MemoryUpdateMemoryParams -from .connection_get_by_tag_params import ConnectionGetByTagParams as ConnectionGetByTagParams -from .connection_configure_response import ConnectionConfigureResponse as ConnectionConfigureResponse -from .connection_get_by_id_response import ConnectionGetByIDResponse as ConnectionGetByIDResponse -from .connection_resources_response import ConnectionResourcesResponse as ConnectionResourcesResponse -from .document_delete_bulk_response import DocumentDeleteBulkResponse as DocumentDeleteBulkResponse -from .document_upload_file_response import DocumentUploadFileResponse as DocumentUploadFileResponse -from .memory_update_memory_response import MemoryUpdateMemoryResponse as MemoryUpdateMemoryResponse -from .connection_delete_by_id_params import ConnectionDeleteByIDParams as ConnectionDeleteByIDParams -from .connection_get_by_tag_response import ConnectionGetByTagResponse as ConnectionGetByTagResponse -from .document_list_processing_params import DocumentListProcessingParams as DocumentListProcessingParams -from .connection_delete_by_id_response import ConnectionDeleteByIDResponse as ConnectionDeleteByIDResponse -from .connection_list_documents_params import ConnectionListDocumentsParams as ConnectionListDocumentsParams -from .document_list_processing_response import DocumentListProcessingResponse as DocumentListProcessingResponse -from .connection_list_documents_response import ConnectionListDocumentsResponse as ConnectionListDocumentsResponse -from .connection_delete_by_provider_params import ConnectionDeleteByProviderParams as ConnectionDeleteByProviderParams -from .connection_delete_by_provider_response import ( - ConnectionDeleteByProviderResponse as ConnectionDeleteByProviderResponse, -) +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .bad_request_error_body import BadRequestErrorBody + from .batch_add_item_result import BatchAddItemResult + from .batch_add_result import BatchAddResult + from .chunk import Chunk + from .chunk_list_item import ChunkListItem + from .chunk_system import ChunkSystem + from .connector import Connector + from .connector_capabilities import ConnectorCapabilities + from .connector_config_value import ConnectorConfigValue + from .connector_deleted import ConnectorDeleted + from .connector_error_code import ConnectorErrorCode + from .connector_last_sync import ConnectorLastSync + from .connector_list import ConnectorList + from .connector_picker import ConnectorPicker + from .connector_provider import ConnectorProvider + from .connector_selection import ConnectorSelection + from .connector_selection_item import ConnectorSelectionItem + from .connector_setup import ( + ConnectorSetup, + ConnectorSetup_Github, + ConnectorSetup_Gmail, + ConnectorSetup_GoogleDrive, + ConnectorSetup_Granola, + ConnectorSetup_Notion, + ConnectorSetup_Onedrive, + ConnectorSetup_S3, + ConnectorSetup_WebCrawler, + ) + from .connector_setup_github import ConnectorSetupGithub + from .connector_setup_gmail import ConnectorSetupGmail + from .connector_setup_google_drive import ConnectorSetupGoogleDrive + from .connector_setup_granola import ConnectorSetupGranola + from .connector_setup_notion import ConnectorSetupNotion + from .connector_setup_onedrive import ConnectorSetupOnedrive + from .connector_setup_result import ConnectorSetupResult + from .connector_setup_s3 import ConnectorSetupS3 + from .connector_setup_web_crawler import ConnectorSetupWebCrawler + from .connector_status import ConnectorStatus + from .connector_sync import ConnectorSync + from .connector_sync_failure import ConnectorSyncFailure + from .connector_sync_started import ConnectorSyncStarted + from .connector_sync_status import ConnectorSyncStatus + from .connector_sync_trigger import ConnectorSyncTrigger + from .delete_document_error import DeleteDocumentError + from .delete_documents_result import DeleteDocumentsResult + from .document import Document + from .document_attachment import DocumentAttachment + from .document_input import DocumentInput + from .document_list_item import DocumentListItem + from .document_ref import DocumentRef + from .document_system import DocumentSystem + from .dreaming_mode import DreamingMode + from .error_response import ErrorResponse + from .file_type import FileType + from .file_upload_result import FileUploadResult + from .filter_and import FilterAnd + from .filter_array_contains import FilterArrayContains + from .filter_array_contains_operator import FilterArrayContainsOperator + from .filter_expression import FilterExpression + from .filter_number_compare import FilterNumberCompare + from .filter_number_compare_operator import FilterNumberCompareOperator + from .filter_or import FilterOr + from .filter_predicate import FilterPredicate + from .filter_string_contains import FilterStringContains + from .filter_string_contains_operator import FilterStringContainsOperator + from .filter_string_equals import FilterStringEquals + from .filter_string_equals_operator import FilterStringEqualsOperator + from .filter_value_equals import FilterValueEquals + from .filter_value_equals_operator import FilterValueEqualsOperator + from .filter_value_equals_value import FilterValueEqualsValue + from .forget_error import ForgetError + from .forget_match import ForgetMatch + from .forget_result import ForgetResult + from .google_drive_config import GoogleDriveConfig + from .google_drive_config_sync_scope import GoogleDriveConfigSyncScope + from .granola_config import GranolaConfig + from .list_request_limit import ListRequestLimit + from .list_request_page import ListRequestPage + from .list_response import ListResponse + from .list_sort import ListSort + from .list_type import ListType + from .memory import Memory + from .memory_relation import MemoryRelation + from .memory_system import MemorySystem + from .metadata_value import MetadataValue + from .namespace import Namespace + from .namespace_deleted import NamespaceDeleted + from .namespace_details import NamespaceDetails + from .namespace_system import NamespaceSystem + from .organization import Organization + from .pagination import Pagination + from .profile import Profile + from .profile_buckets import ProfileBuckets + from .profile_fact import ProfileFact + from .profile_response import ProfileResponse + from .related_memories import RelatedMemories + from .related_memory import RelatedMemory + from .related_memory_system import RelatedMemorySystem + from .s3config import S3Config + from .search_mode import SearchMode + from .search_request_attach import SearchRequestAttach + from .search_request_rerank import SearchRequestRerank + from .search_response import SearchResponse + from .search_result import SearchResult + from .search_result_included import SearchResultIncluded + from .search_result_included_document import SearchResultIncludedDocument + from .search_result_system import SearchResultSystem + from .sort_order import SortOrder + from .task_type import TaskType + from .validation_error_response import ValidationErrorResponse + from .validation_error_response_error_item import ValidationErrorResponseErrorItem + from .validation_error_response_error_item_path_item import ValidationErrorResponseErrorItemPathItem + from .web_crawler_config import WebCrawlerConfig +_dynamic_imports: typing.Dict[str, str] = { + "BadRequestErrorBody": ".bad_request_error_body", + "BatchAddItemResult": ".batch_add_item_result", + "BatchAddResult": ".batch_add_result", + "Chunk": ".chunk", + "ChunkListItem": ".chunk_list_item", + "ChunkSystem": ".chunk_system", + "Connector": ".connector", + "ConnectorCapabilities": ".connector_capabilities", + "ConnectorConfigValue": ".connector_config_value", + "ConnectorDeleted": ".connector_deleted", + "ConnectorErrorCode": ".connector_error_code", + "ConnectorLastSync": ".connector_last_sync", + "ConnectorList": ".connector_list", + "ConnectorPicker": ".connector_picker", + "ConnectorProvider": ".connector_provider", + "ConnectorSelection": ".connector_selection", + "ConnectorSelectionItem": ".connector_selection_item", + "ConnectorSetup": ".connector_setup", + "ConnectorSetupGithub": ".connector_setup_github", + "ConnectorSetupGmail": ".connector_setup_gmail", + "ConnectorSetupGoogleDrive": ".connector_setup_google_drive", + "ConnectorSetupGranola": ".connector_setup_granola", + "ConnectorSetupNotion": ".connector_setup_notion", + "ConnectorSetupOnedrive": ".connector_setup_onedrive", + "ConnectorSetupResult": ".connector_setup_result", + "ConnectorSetupS3": ".connector_setup_s3", + "ConnectorSetupWebCrawler": ".connector_setup_web_crawler", + "ConnectorSetup_Github": ".connector_setup", + "ConnectorSetup_Gmail": ".connector_setup", + "ConnectorSetup_GoogleDrive": ".connector_setup", + "ConnectorSetup_Granola": ".connector_setup", + "ConnectorSetup_Notion": ".connector_setup", + "ConnectorSetup_Onedrive": ".connector_setup", + "ConnectorSetup_S3": ".connector_setup", + "ConnectorSetup_WebCrawler": ".connector_setup", + "ConnectorStatus": ".connector_status", + "ConnectorSync": ".connector_sync", + "ConnectorSyncFailure": ".connector_sync_failure", + "ConnectorSyncStarted": ".connector_sync_started", + "ConnectorSyncStatus": ".connector_sync_status", + "ConnectorSyncTrigger": ".connector_sync_trigger", + "DeleteDocumentError": ".delete_document_error", + "DeleteDocumentsResult": ".delete_documents_result", + "Document": ".document", + "DocumentAttachment": ".document_attachment", + "DocumentInput": ".document_input", + "DocumentListItem": ".document_list_item", + "DocumentRef": ".document_ref", + "DocumentSystem": ".document_system", + "DreamingMode": ".dreaming_mode", + "ErrorResponse": ".error_response", + "FileType": ".file_type", + "FileUploadResult": ".file_upload_result", + "FilterAnd": ".filter_and", + "FilterArrayContains": ".filter_array_contains", + "FilterArrayContainsOperator": ".filter_array_contains_operator", + "FilterExpression": ".filter_expression", + "FilterNumberCompare": ".filter_number_compare", + "FilterNumberCompareOperator": ".filter_number_compare_operator", + "FilterOr": ".filter_or", + "FilterPredicate": ".filter_predicate", + "FilterStringContains": ".filter_string_contains", + "FilterStringContainsOperator": ".filter_string_contains_operator", + "FilterStringEquals": ".filter_string_equals", + "FilterStringEqualsOperator": ".filter_string_equals_operator", + "FilterValueEquals": ".filter_value_equals", + "FilterValueEqualsOperator": ".filter_value_equals_operator", + "FilterValueEqualsValue": ".filter_value_equals_value", + "ForgetError": ".forget_error", + "ForgetMatch": ".forget_match", + "ForgetResult": ".forget_result", + "GoogleDriveConfig": ".google_drive_config", + "GoogleDriveConfigSyncScope": ".google_drive_config_sync_scope", + "GranolaConfig": ".granola_config", + "ListRequestLimit": ".list_request_limit", + "ListRequestPage": ".list_request_page", + "ListResponse": ".list_response", + "ListSort": ".list_sort", + "ListType": ".list_type", + "Memory": ".memory", + "MemoryRelation": ".memory_relation", + "MemorySystem": ".memory_system", + "MetadataValue": ".metadata_value", + "Namespace": ".namespace", + "NamespaceDeleted": ".namespace_deleted", + "NamespaceDetails": ".namespace_details", + "NamespaceSystem": ".namespace_system", + "Organization": ".organization", + "Pagination": ".pagination", + "Profile": ".profile", + "ProfileBuckets": ".profile_buckets", + "ProfileFact": ".profile_fact", + "ProfileResponse": ".profile_response", + "RelatedMemories": ".related_memories", + "RelatedMemory": ".related_memory", + "RelatedMemorySystem": ".related_memory_system", + "S3Config": ".s3config", + "SearchMode": ".search_mode", + "SearchRequestAttach": ".search_request_attach", + "SearchRequestRerank": ".search_request_rerank", + "SearchResponse": ".search_response", + "SearchResult": ".search_result", + "SearchResultIncluded": ".search_result_included", + "SearchResultIncludedDocument": ".search_result_included_document", + "SearchResultSystem": ".search_result_system", + "SortOrder": ".sort_order", + "TaskType": ".task_type", + "ValidationErrorResponse": ".validation_error_response", + "ValidationErrorResponseErrorItem": ".validation_error_response_error_item", + "ValidationErrorResponseErrorItemPathItem": ".validation_error_response_error_item_path_item", + "WebCrawlerConfig": ".web_crawler_config", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "BadRequestErrorBody", + "BatchAddItemResult", + "BatchAddResult", + "Chunk", + "ChunkListItem", + "ChunkSystem", + "Connector", + "ConnectorCapabilities", + "ConnectorConfigValue", + "ConnectorDeleted", + "ConnectorErrorCode", + "ConnectorLastSync", + "ConnectorList", + "ConnectorPicker", + "ConnectorProvider", + "ConnectorSelection", + "ConnectorSelectionItem", + "ConnectorSetup", + "ConnectorSetupGithub", + "ConnectorSetupGmail", + "ConnectorSetupGoogleDrive", + "ConnectorSetupGranola", + "ConnectorSetupNotion", + "ConnectorSetupOnedrive", + "ConnectorSetupResult", + "ConnectorSetupS3", + "ConnectorSetupWebCrawler", + "ConnectorSetup_Github", + "ConnectorSetup_Gmail", + "ConnectorSetup_GoogleDrive", + "ConnectorSetup_Granola", + "ConnectorSetup_Notion", + "ConnectorSetup_Onedrive", + "ConnectorSetup_S3", + "ConnectorSetup_WebCrawler", + "ConnectorStatus", + "ConnectorSync", + "ConnectorSyncFailure", + "ConnectorSyncStarted", + "ConnectorSyncStatus", + "ConnectorSyncTrigger", + "DeleteDocumentError", + "DeleteDocumentsResult", + "Document", + "DocumentAttachment", + "DocumentInput", + "DocumentListItem", + "DocumentRef", + "DocumentSystem", + "DreamingMode", + "ErrorResponse", + "FileType", + "FileUploadResult", + "FilterAnd", + "FilterArrayContains", + "FilterArrayContainsOperator", + "FilterExpression", + "FilterNumberCompare", + "FilterNumberCompareOperator", + "FilterOr", + "FilterPredicate", + "FilterStringContains", + "FilterStringContainsOperator", + "FilterStringEquals", + "FilterStringEqualsOperator", + "FilterValueEquals", + "FilterValueEqualsOperator", + "FilterValueEqualsValue", + "ForgetError", + "ForgetMatch", + "ForgetResult", + "GoogleDriveConfig", + "GoogleDriveConfigSyncScope", + "GranolaConfig", + "ListRequestLimit", + "ListRequestPage", + "ListResponse", + "ListSort", + "ListType", + "Memory", + "MemoryRelation", + "MemorySystem", + "MetadataValue", + "Namespace", + "NamespaceDeleted", + "NamespaceDetails", + "NamespaceSystem", + "Organization", + "Pagination", + "Profile", + "ProfileBuckets", + "ProfileFact", + "ProfileResponse", + "RelatedMemories", + "RelatedMemory", + "RelatedMemorySystem", + "S3Config", + "SearchMode", + "SearchRequestAttach", + "SearchRequestRerank", + "SearchResponse", + "SearchResult", + "SearchResultIncluded", + "SearchResultIncludedDocument", + "SearchResultSystem", + "SortOrder", + "TaskType", + "ValidationErrorResponse", + "ValidationErrorResponseErrorItem", + "ValidationErrorResponseErrorItemPathItem", + "WebCrawlerConfig", +] diff --git a/src/supermemory/types/add_response.py b/src/supermemory/types/add_response.py deleted file mode 100644 index cfa9c5fb..00000000 --- a/src/supermemory/types/add_response.py +++ /dev/null @@ -1,13 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from .._models import BaseModel - -__all__ = ["AddResponse"] - - -class AddResponse(BaseModel): - id: str - """Unique identifier of the document""" - - status: str - """Status of the document""" diff --git a/src/supermemory/types/bad_request_error_body.py b/src/supermemory/types/bad_request_error_body.py new file mode 100644 index 00000000..8389a7b3 --- /dev/null +++ b/src/supermemory/types/bad_request_error_body.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .error_response import ErrorResponse +from .validation_error_response import ValidationErrorResponse + +BadRequestErrorBody = typing.Union[ErrorResponse, ValidationErrorResponse] diff --git a/src/supermemory/types/batch_add_item_result.py b/src/supermemory/types/batch_add_item_result.py new file mode 100644 index 00000000..c6ecca43 --- /dev/null +++ b/src/supermemory/types/batch_add_item_result.py @@ -0,0 +1,43 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class BatchAddItemResult(UncheckedBaseModel): + id: str = pydantic.Field() + """ + Unique identifier of the document (empty string for failed items) + """ + + status: str = pydantic.Field() + """ + Status of the document (e.g. 'done', 'queued', 'error') + """ + + error: typing.Optional[str] = pydantic.Field(default=None) + """ + Error message when status is 'error' + """ + + details: typing.Optional[str] = pydantic.Field(default=None) + """ + Additional error details when status is 'error' + """ + + url: typing.Optional[str] = pydantic.Field(default=None) + """ + URL of the failed item, when it had one. Only present on failed items, where it may be the only way to tell which input the error belongs to (the id falls back to 'unknown' when the input had neither an id nor a customId). + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/batch_add_result.py b/src/supermemory/types/batch_add_result.py new file mode 100644 index 00000000..d92f753c --- /dev/null +++ b/src/supermemory/types/batch_add_result.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .batch_add_item_result import BatchAddItemResult + + +class BatchAddResult(UncheckedBaseModel): + results: typing.List[BatchAddItemResult] = pydantic.Field() + """ + Array of results for each document in the batch + """ + + failed: float = pydantic.Field() + """ + Count of documents that failed to add + """ + + success: float = pydantic.Field() + """ + Count of documents successfully added + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/chunk.py b/src/supermemory/types/chunk.py new file mode 100644 index 00000000..3a8c1f85 --- /dev/null +++ b/src/supermemory/types/chunk.py @@ -0,0 +1,53 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .chunk_system import ChunkSystem + + +class Chunk(UncheckedBaseModel): + """ + A chunk of source content belonging to a document + """ + + id: str = pydantic.Field() + """ + Stable chunk identifier + """ + + position: int = pydantic.Field() + """ + Zero-based order within the document + """ + + content: str = pydantic.Field() + """ + Chunk text used for retrieval + """ + + type: str = pydantic.Field() + """ + Chunk content type + """ + + metadata: typing.Optional[typing.Dict[str, typing.Any]] = pydantic.Field(default=None) + """ + Chunk metadata, when available + """ + + system: ChunkSystem = pydantic.Field() + """ + Lifecycle timestamps maintained by Supermemory + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/chunk_list_item.py b/src/supermemory/types/chunk_list_item.py new file mode 100644 index 00000000..9ebee845 --- /dev/null +++ b/src/supermemory/types/chunk_list_item.py @@ -0,0 +1,60 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .chunk_system import ChunkSystem + + +class ChunkListItem(UncheckedBaseModel): + id: str = pydantic.Field() + """ + Stable chunk identifier + """ + + position: int = pydantic.Field() + """ + Zero-based order within the document + """ + + content: str = pydantic.Field() + """ + Chunk text used for retrieval + """ + + type: str = pydantic.Field() + """ + Chunk content type + """ + + metadata: typing.Optional[typing.Dict[str, typing.Any]] = pydantic.Field(default=None) + """ + Chunk metadata, when available + """ + + system: ChunkSystem = pydantic.Field() + """ + Lifecycle timestamps maintained by Supermemory + """ + + document_id: typing_extensions.Annotated[ + str, + FieldMetadata(alias="documentId"), + pydantic.Field(alias="documentId", description="ID of the chunk's parent document"), + ] + """ + ID of the chunk's parent document + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/chunk_system.py b/src/supermemory/types/chunk_system.py new file mode 100644 index 00000000..99b6076a --- /dev/null +++ b/src/supermemory/types/chunk_system.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ChunkSystem(UncheckedBaseModel): + """ + Lifecycle timestamps maintained by Supermemory + """ + + created_at: typing_extensions.Annotated[ + dt.datetime, + FieldMetadata(alias="createdAt"), + pydantic.Field(alias="createdAt", description="ISO 8601 timestamp when the chunk was created"), + ] + """ + ISO 8601 timestamp when the chunk was created + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/client_add_params.py b/src/supermemory/types/client_add_params.py deleted file mode 100644 index 5d181a3c..00000000 --- a/src/supermemory/types/client_add_params.py +++ /dev/null @@ -1,80 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Dict, Union -from typing_extensions import Literal, Required, Annotated, TypedDict - -from .._types import SequenceNotStr -from .._utils import PropertyInfo - -__all__ = ["ClientAddParams"] - - -class ClientAddParams(TypedDict, total=False): - content: Required[str] - """The content to extract and process into a document. - - This can be a URL to a website, a PDF, an image, or a video. - """ - - container_tag: Annotated[str, PropertyInfo(alias="containerTag")] - """Optional tag this document should be containerized by. - - Max 100 characters, alphanumeric with hyphens, underscores, and dots only. - """ - - container_tags: Annotated[SequenceNotStr[str], PropertyInfo(alias="containerTags")] - - custom_id: Annotated[str, PropertyInfo(alias="customId")] - """Optional custom ID of the document. - - Max 100 characters, alphanumeric with hyphens, underscores, and dots only. - """ - - document_date: Annotated[str, PropertyInfo(alias="documentDate")] - """When this document's content is from, as opposed to when it was uploaded. - - Accepts YYYY-MM-DD or a full ISO 8601 timestamp. Memory extraction resolves - relative dates against this instead of the ingestion time, and documents in a - batch are processed oldest-first so newer facts correctly supersede older ones. - Set this whenever you backfill historical content. - """ - - dreaming: Literal["instant", "dynamic"] - """Processing mode. - - "dynamic" (default) groups related documents together so memories form from - coherent, logical units rather than one isolated entry at a time. "instant" - processes each document on its own right away, and bills one extra operation per - document. - """ - - entity_context: Annotated[str, PropertyInfo(alias="entityContext")] - """Optional entity context for this container tag. - - Max 1500 characters. Used during document processing to guide memory extraction. - """ - - filepath: str - """Optional file path for the document. - - Used by supermemoryfs to store the full path of the file. - """ - - filter_by_metadata: Annotated[ - Dict[str, Union[str, float, bool, SequenceNotStr[str]]], PropertyInfo(alias="filterByMetadata") - ] - """ - Optional metadata filter to apply when pulling related memories and profile - during ingestion. Only memories matching these filters will be used as context. - """ - - metadata: Dict[str, Union[str, float, bool, SequenceNotStr[str]]] - """Optional metadata for the document.""" - - task_type: Annotated[Literal["memory", "superrag"], PropertyInfo(alias="taskType")] - """ - Task type: "memory" (default) for full context layer with SuperRAG built in, - "superrag" for managed RAG as a service. - """ diff --git a/src/supermemory/types/client_profile_params.py b/src/supermemory/types/client_profile_params.py deleted file mode 100644 index 93488e72..00000000 --- a/src/supermemory/types/client_profile_params.py +++ /dev/null @@ -1,140 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import List, Union, Iterable -from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict - -from .._types import SequenceNotStr -from .._utils import PropertyInfo - -__all__ = [ - "ClientProfileParams", - "Filters", - "FiltersOr", - "FiltersOrOr", - "FiltersOrOrFilterCondition", - "FiltersOrOrOr", - "FiltersOrOrAnd", - "FiltersAnd", - "FiltersAndAnd", - "FiltersAndAndFilterCondition", - "FiltersAndAndOr", - "FiltersAndAndAnd", -] - - -class ClientProfileParams(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. - """ - - buckets: SequenceNotStr[str] - """Specific bucket keys to return. - - Omit to return all configured buckets. Only relevant when "buckets" is included. - """ - - filters: Filters - """Optional metadata filters to apply to profile results and search results. - - Supports complex AND/OR queries with multiple conditions. - """ - - include: List[Literal["static", "dynamic", "buckets"]] - """Profile sections to return. - - Omit to return all sections. Pass a subset to reduce payload — e.g. ["buckets"] - skips static and dynamic entirely. - """ - - q: str - """Optional search query to include search results in the response""" - - threshold: float - """Threshold for search results. - - Only results with a score above this threshold will be included. - """ - - -class FiltersOrOrFilterCondition(TypedDict, total=False): - """ - A single filter condition based on metadata, numeric values, array contents, or string matching - """ - - key: Required[str] - - value: Required[str] - - filter_type: Annotated[ - Literal["metadata", "numeric", "array_contains", "string_contains"], PropertyInfo(alias="filterType") - ] - - ignore_case: Annotated[Union[bool, Literal["true", "false"]], PropertyInfo(alias="ignoreCase")] - - negate: Union[bool, Literal["true", "false"]] - - numeric_operator: Annotated[Literal[">", "<", ">=", "<=", "="], PropertyInfo(alias="numericOperator")] - - -class FiltersOrOrOr(TypedDict, total=False): - or_: Required[Annotated[Iterable[object], PropertyInfo(alias="OR")]] - """OR: Array of conditions or nested expressions""" - - -class FiltersOrOrAnd(TypedDict, total=False): - and_: Required[Annotated[Iterable[object], PropertyInfo(alias="AND")]] - """AND: Array of conditions or nested expressions""" - - -FiltersOrOr: TypeAlias = Union[FiltersOrOrFilterCondition, FiltersOrOrOr, FiltersOrOrAnd] - - -class FiltersOr(TypedDict, total=False): - or_: Required[Annotated[Iterable[FiltersOrOr], PropertyInfo(alias="OR")]] - """Array of OR filter expressions""" - - -class FiltersAndAndFilterCondition(TypedDict, total=False): - """ - A single filter condition based on metadata, numeric values, array contents, or string matching - """ - - key: Required[str] - - value: Required[str] - - filter_type: Annotated[ - Literal["metadata", "numeric", "array_contains", "string_contains"], PropertyInfo(alias="filterType") - ] - - ignore_case: Annotated[Union[bool, Literal["true", "false"]], PropertyInfo(alias="ignoreCase")] - - negate: Union[bool, Literal["true", "false"]] - - numeric_operator: Annotated[Literal[">", "<", ">=", "<=", "="], PropertyInfo(alias="numericOperator")] - - -class FiltersAndAndOr(TypedDict, total=False): - or_: Required[Annotated[Iterable[object], PropertyInfo(alias="OR")]] - """OR: Array of conditions or nested expressions""" - - -class FiltersAndAndAnd(TypedDict, total=False): - and_: Required[Annotated[Iterable[object], PropertyInfo(alias="AND")]] - """AND: Array of conditions or nested expressions""" - - -FiltersAndAnd: TypeAlias = Union[FiltersAndAndFilterCondition, FiltersAndAndOr, FiltersAndAndAnd] - - -class FiltersAnd(TypedDict, total=False): - and_: Required[Annotated[Iterable[FiltersAndAnd], PropertyInfo(alias="AND")]] - """Array of AND filter expressions""" - - -Filters: TypeAlias = Union[FiltersOr, FiltersAnd] diff --git a/src/supermemory/types/connection_configure_params.py b/src/supermemory/types/connection_configure_params.py deleted file mode 100644 index 224ed1d2..00000000 --- a/src/supermemory/types/connection_configure_params.py +++ /dev/null @@ -1,12 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Dict, Iterable -from typing_extensions import Required, TypedDict - -__all__ = ["ConnectionConfigureParams"] - - -class ConnectionConfigureParams(TypedDict, total=False): - resources: Required[Iterable[Dict[str, object]]] diff --git a/src/supermemory/types/connection_configure_response.py b/src/supermemory/types/connection_configure_response.py deleted file mode 100644 index 065c4a87..00000000 --- a/src/supermemory/types/connection_configure_response.py +++ /dev/null @@ -1,17 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["ConnectionConfigureResponse"] - - -class ConnectionConfigureResponse(BaseModel): - message: str - - success: bool - - webhooks_registered: Optional[float] = FieldInfo(alias="webhooksRegistered", default=None) diff --git a/src/supermemory/types/connection_create_params.py b/src/supermemory/types/connection_create_params.py deleted file mode 100644 index fcd9923d..00000000 --- a/src/supermemory/types/connection_create_params.py +++ /dev/null @@ -1,23 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Dict, Union, Optional -from typing_extensions import Annotated, TypedDict - -from .._types import SequenceNotStr -from .._utils import PropertyInfo - -__all__ = ["ConnectionCreateParams"] - - -class ConnectionCreateParams(TypedDict, total=False): - container_tag: Annotated[str, PropertyInfo(alias="containerTag")] - - container_tags: Annotated[SequenceNotStr[str], PropertyInfo(alias="containerTags")] - - document_limit: Annotated[int, PropertyInfo(alias="documentLimit")] - - metadata: Optional[Dict[str, Union[str, float, bool]]] - - redirect_url: Annotated[str, PropertyInfo(alias="redirectUrl")] diff --git a/src/supermemory/types/connection_create_response.py b/src/supermemory/types/connection_create_response.py deleted file mode 100644 index 6f886920..00000000 --- a/src/supermemory/types/connection_create_response.py +++ /dev/null @@ -1,19 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["ConnectionCreateResponse"] - - -class ConnectionCreateResponse(BaseModel): - id: str - - auth_link: Optional[str] = FieldInfo(alias="authLink", default=None) - - expires_in: Optional[str] = FieldInfo(alias="expiresIn", default=None) - - redirects_to: Optional[str] = FieldInfo(alias="redirectsTo", default=None) diff --git a/src/supermemory/types/connection_delete_by_id_params.py b/src/supermemory/types/connection_delete_by_id_params.py deleted file mode 100644 index 2f08f960..00000000 --- a/src/supermemory/types/connection_delete_by_id_params.py +++ /dev/null @@ -1,14 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Annotated, TypedDict - -from .._utils import PropertyInfo - -__all__ = ["ConnectionDeleteByIDParams"] - - -class ConnectionDeleteByIDParams(TypedDict, total=False): - delete_documents: Annotated[str, PropertyInfo(alias="deleteDocuments")] - """Whether to also delete documents imported by this connection. Defaults to true.""" diff --git a/src/supermemory/types/connection_delete_by_id_response.py b/src/supermemory/types/connection_delete_by_id_response.py deleted file mode 100644 index 4b18f30f..00000000 --- a/src/supermemory/types/connection_delete_by_id_response.py +++ /dev/null @@ -1,17 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List -from typing_extensions import TypeAlias - -from .._models import BaseModel - -__all__ = ["ConnectionDeleteByIDResponse", "ConnectionDeleteByIDResponseItem"] - - -class ConnectionDeleteByIDResponseItem(BaseModel): - id: str - - provider: str - - -ConnectionDeleteByIDResponse: TypeAlias = List[ConnectionDeleteByIDResponseItem] diff --git a/src/supermemory/types/connection_delete_by_provider_params.py b/src/supermemory/types/connection_delete_by_provider_params.py deleted file mode 100644 index 09e136df..00000000 --- a/src/supermemory/types/connection_delete_by_provider_params.py +++ /dev/null @@ -1,15 +0,0 @@ -# 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 .._types import SequenceNotStr -from .._utils import PropertyInfo - -__all__ = ["ConnectionDeleteByProviderParams"] - - -class ConnectionDeleteByProviderParams(TypedDict, total=False): - container_tags: Required[Annotated[SequenceNotStr[str], PropertyInfo(alias="containerTags")]] - """Optional comma-separated list of container tags to filter connections by""" diff --git a/src/supermemory/types/connection_delete_by_provider_response.py b/src/supermemory/types/connection_delete_by_provider_response.py deleted file mode 100644 index 23c8202e..00000000 --- a/src/supermemory/types/connection_delete_by_provider_response.py +++ /dev/null @@ -1,17 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List -from typing_extensions import TypeAlias - -from .._models import BaseModel - -__all__ = ["ConnectionDeleteByProviderResponse", "ConnectionDeleteByProviderResponseItem"] - - -class ConnectionDeleteByProviderResponseItem(BaseModel): - id: str - - provider: str - - -ConnectionDeleteByProviderResponse: TypeAlias = List[ConnectionDeleteByProviderResponseItem] diff --git a/src/supermemory/types/connection_get_by_id_response.py b/src/supermemory/types/connection_get_by_id_response.py deleted file mode 100644 index 2ddef438..00000000 --- a/src/supermemory/types/connection_get_by_id_response.py +++ /dev/null @@ -1,44 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict, List, Optional -from typing_extensions import Literal - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["ConnectionGetByIDResponse", "LastSyncRun"] - - -class LastSyncRun(BaseModel): - status: str - - completed_at: Optional[str] = FieldInfo(alias="completedAt", default=None) - - error: Optional[str] = None - - error_code: Optional[ - Literal["auth_expired", "rate_limited", "plan_required", "provider_unavailable", "internal"] - ] = FieldInfo(alias="errorCode", default=None) - - started_at: Optional[str] = FieldInfo(alias="startedAt", default=None) - - -class ConnectionGetByIDResponse(BaseModel): - id: str - - created_at: str = FieldInfo(alias="createdAt") - - provider: str - - container_tags: Optional[List[str]] = FieldInfo(alias="containerTags", default=None) - - document_limit: Optional[float] = FieldInfo(alias="documentLimit", default=None) - - email: Optional[str] = None - - expires_at: Optional[str] = FieldInfo(alias="expiresAt", default=None) - - last_sync_run: Optional[LastSyncRun] = FieldInfo(alias="lastSyncRun", default=None) - - metadata: Optional[Dict[str, object]] = None diff --git a/src/supermemory/types/connection_get_by_tag_params.py b/src/supermemory/types/connection_get_by_tag_params.py deleted file mode 100644 index 13a90822..00000000 --- a/src/supermemory/types/connection_get_by_tag_params.py +++ /dev/null @@ -1,15 +0,0 @@ -# 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 .._types import SequenceNotStr -from .._utils import PropertyInfo - -__all__ = ["ConnectionGetByTagParams"] - - -class ConnectionGetByTagParams(TypedDict, total=False): - container_tags: Required[Annotated[SequenceNotStr[str], PropertyInfo(alias="containerTags")]] - """Comma-separated list of container tags to filter connection by""" diff --git a/src/supermemory/types/connection_get_by_tag_response.py b/src/supermemory/types/connection_get_by_tag_response.py deleted file mode 100644 index 161cac0c..00000000 --- a/src/supermemory/types/connection_get_by_tag_response.py +++ /dev/null @@ -1,44 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict, List, Optional -from typing_extensions import Literal - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["ConnectionGetByTagResponse", "LastSyncRun"] - - -class LastSyncRun(BaseModel): - status: str - - completed_at: Optional[str] = FieldInfo(alias="completedAt", default=None) - - error: Optional[str] = None - - error_code: Optional[ - Literal["auth_expired", "rate_limited", "plan_required", "provider_unavailable", "internal"] - ] = FieldInfo(alias="errorCode", default=None) - - started_at: Optional[str] = FieldInfo(alias="startedAt", default=None) - - -class ConnectionGetByTagResponse(BaseModel): - id: str - - created_at: str = FieldInfo(alias="createdAt") - - provider: str - - container_tags: Optional[List[str]] = FieldInfo(alias="containerTags", default=None) - - document_limit: Optional[float] = FieldInfo(alias="documentLimit", default=None) - - email: Optional[str] = None - - expires_at: Optional[str] = FieldInfo(alias="expiresAt", default=None) - - last_sync_run: Optional[LastSyncRun] = FieldInfo(alias="lastSyncRun", default=None) - - metadata: Optional[Dict[str, object]] = None diff --git a/src/supermemory/types/connection_import_params.py b/src/supermemory/types/connection_import_params.py deleted file mode 100644 index 2e19b220..00000000 --- a/src/supermemory/types/connection_import_params.py +++ /dev/null @@ -1,15 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Annotated, TypedDict - -from .._types import SequenceNotStr -from .._utils import PropertyInfo - -__all__ = ["ConnectionImportParams"] - - -class ConnectionImportParams(TypedDict, total=False): - container_tags: Annotated[SequenceNotStr[str], PropertyInfo(alias="containerTags")] - """Optional comma-separated list of container tags to filter connections by""" diff --git a/src/supermemory/types/connection_import_response.py b/src/supermemory/types/connection_import_response.py deleted file mode 100644 index 9e5445be..00000000 --- a/src/supermemory/types/connection_import_response.py +++ /dev/null @@ -1,7 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing_extensions import TypeAlias - -__all__ = ["ConnectionImportResponse"] - -ConnectionImportResponse: TypeAlias = str diff --git a/src/supermemory/types/connection_list_documents_params.py b/src/supermemory/types/connection_list_documents_params.py deleted file mode 100644 index 8673d26d..00000000 --- a/src/supermemory/types/connection_list_documents_params.py +++ /dev/null @@ -1,15 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Annotated, TypedDict - -from .._types import SequenceNotStr -from .._utils import PropertyInfo - -__all__ = ["ConnectionListDocumentsParams"] - - -class ConnectionListDocumentsParams(TypedDict, total=False): - container_tags: Annotated[SequenceNotStr[str], PropertyInfo(alias="containerTags")] - """Optional comma-separated list of container tags to filter documents by""" diff --git a/src/supermemory/types/connection_list_documents_response.py b/src/supermemory/types/connection_list_documents_response.py deleted file mode 100644 index 58ac85ad..00000000 --- a/src/supermemory/types/connection_list_documents_response.py +++ /dev/null @@ -1,29 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional -from typing_extensions import TypeAlias - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["ConnectionListDocumentsResponse", "ConnectionListDocumentsResponseItem"] - - -class ConnectionListDocumentsResponseItem(BaseModel): - id: Optional[str] = None - - created_at: str = FieldInfo(alias="createdAt") - - status: str - - summary: Optional[str] = None - - title: Optional[str] = None - - type: str - - updated_at: str = FieldInfo(alias="updatedAt") - - -ConnectionListDocumentsResponse: TypeAlias = List[ConnectionListDocumentsResponseItem] diff --git a/src/supermemory/types/connection_list_params.py b/src/supermemory/types/connection_list_params.py deleted file mode 100644 index 2948fa31..00000000 --- a/src/supermemory/types/connection_list_params.py +++ /dev/null @@ -1,15 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Annotated, TypedDict - -from .._types import SequenceNotStr -from .._utils import PropertyInfo - -__all__ = ["ConnectionListParams"] - - -class ConnectionListParams(TypedDict, total=False): - container_tags: Annotated[SequenceNotStr[str], PropertyInfo(alias="containerTags")] - """Optional comma-separated list of container tags to filter documents by""" diff --git a/src/supermemory/types/connection_list_response.py b/src/supermemory/types/connection_list_response.py deleted file mode 100644 index f946b5dd..00000000 --- a/src/supermemory/types/connection_list_response.py +++ /dev/null @@ -1,47 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict, List, Optional -from typing_extensions import Literal, TypeAlias - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["ConnectionListResponse", "ConnectionListResponseItem", "ConnectionListResponseItemLastSyncRun"] - - -class ConnectionListResponseItemLastSyncRun(BaseModel): - status: str - - completed_at: Optional[str] = FieldInfo(alias="completedAt", default=None) - - error: Optional[str] = None - - error_code: Optional[ - Literal["auth_expired", "rate_limited", "plan_required", "provider_unavailable", "internal"] - ] = FieldInfo(alias="errorCode", default=None) - - started_at: Optional[str] = FieldInfo(alias="startedAt", default=None) - - -class ConnectionListResponseItem(BaseModel): - id: str - - created_at: str = FieldInfo(alias="createdAt") - - provider: str - - container_tags: Optional[List[str]] = FieldInfo(alias="containerTags", default=None) - - document_limit: Optional[float] = FieldInfo(alias="documentLimit", default=None) - - email: Optional[str] = None - - expires_at: Optional[str] = FieldInfo(alias="expiresAt", default=None) - - last_sync_run: Optional[ConnectionListResponseItemLastSyncRun] = FieldInfo(alias="lastSyncRun", default=None) - - metadata: Optional[Dict[str, object]] = None - - -ConnectionListResponse: TypeAlias = List[ConnectionListResponseItem] diff --git a/src/supermemory/types/connection_resources_params.py b/src/supermemory/types/connection_resources_params.py deleted file mode 100644 index ff39796d..00000000 --- a/src/supermemory/types/connection_resources_params.py +++ /dev/null @@ -1,15 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import TypedDict - -__all__ = ["ConnectionResourcesParams"] - - -class ConnectionResourcesParams(TypedDict, total=False): - page: float - - parent_id: str - - per_page: float diff --git a/src/supermemory/types/connection_resources_response.py b/src/supermemory/types/connection_resources_response.py deleted file mode 100644 index 1d70419a..00000000 --- a/src/supermemory/types/connection_resources_response.py +++ /dev/null @@ -1,13 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict, List, Optional - -from .._models import BaseModel - -__all__ = ["ConnectionResourcesResponse"] - - -class ConnectionResourcesResponse(BaseModel): - resources: List[Dict[str, object]] - - total_count: Optional[float] = None diff --git a/src/supermemory/types/connector.py b/src/supermemory/types/connector.py new file mode 100644 index 00000000..a778f344 --- /dev/null +++ b/src/supermemory/types/connector.py @@ -0,0 +1,121 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .connector_capabilities import ConnectorCapabilities +from .connector_config_value import ConnectorConfigValue +from .connector_last_sync import ConnectorLastSync +from .connector_picker import ConnectorPicker +from .connector_provider import ConnectorProvider +from .connector_selection_item import ConnectorSelectionItem +from .connector_status import ConnectorStatus +from .connector_sync import ConnectorSync + + +class Connector(UncheckedBaseModel): + id: str + provider: ConnectorProvider = pydantic.Field() + """ + External source + """ + + status: ConnectorStatus = pydantic.Field() + """ + pending_authorization until the user finishes the provider login at authUrl; DELETE cancels it. Pending connectors are not listed. + """ + + auth_url: typing_extensions.Annotated[ + typing.Optional[str], + FieldMetadata(alias="authUrl"), + pydantic.Field( + alias="authUrl", + description="Present while pending, when the caller can create connectors in this namespace: where to send the user to authorize", + ), + ] = None + """ + Present while pending, when the caller can create connectors in this namespace: where to send the user to authorize + """ + + auth_url_expires_at: typing_extensions.Annotated[ + typing.Optional[dt.datetime], + FieldMetadata(alias="authUrlExpiresAt"), + pydantic.Field(alias="authUrlExpiresAt", description="Present while pending: when authUrl stops working"), + ] = None + """ + Present while pending: when authUrl stops working + """ + + namespace: typing.Optional[str] = pydantic.Field(default=None) + """ + Namespace the connector syncs into. Null only for connectors created before namespaces were required. + """ + + account: typing.Optional[str] = pydantic.Field(default=None) + """ + Account on the provider side, usually an email + """ + + capabilities: ConnectorCapabilities + config: typing.Optional[typing.Dict[str, typing.Optional[ConnectorConfigValue]]] = pydantic.Field(default=None) + """ + Non-secret setup, such as the S3 bucket, crawler start URL or Notion workspace. Credentials are never returned. + """ + + selection: typing.Optional[typing.Dict[str, typing.Optional[typing.List[ConnectorSelectionItem]]]] = pydantic.Field( + default=None + ) + """ + What syncs, grouped by kind, in the same shape PATCH takes. Null for connectors that sync everything. + """ + + document_limit: typing_extensions.Annotated[ + float, FieldMetadata(alias="documentLimit"), pydantic.Field(alias="documentLimit") + ] + document_count: typing_extensions.Annotated[ + float, FieldMetadata(alias="documentCount"), pydantic.Field(alias="documentCount") + ] + last_sync: typing_extensions.Annotated[ + typing.Optional[ConnectorLastSync], + FieldMetadata(alias="lastSync"), + pydantic.Field(alias="lastSync", description="Most recent sync run"), + ] = None + """ + Most recent sync run + """ + + last_synced_at: typing_extensions.Annotated[ + typing.Optional[dt.datetime], + FieldMetadata(alias="lastSyncedAt"), + pydantic.Field(alias="lastSyncedAt", description="When content last finished syncing"), + ] = None + """ + When content last finished syncing + """ + + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt"), pydantic.Field(alias="createdAt") + ] + syncs: typing.Optional[typing.List[ConnectorSync]] = pydantic.Field(default=None) + """ + Present when attach=syncs + """ + + picker: typing.Optional[ConnectorPicker] = pydantic.Field(default=None) + """ + Present when attach=picker. Null for connectors that sync everything. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/connector_capabilities.py b/src/supermemory/types/connector_capabilities.py new file mode 100644 index 00000000..0f0d81cd --- /dev/null +++ b/src/supermemory/types/connector_capabilities.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ConnectorCapabilities(UncheckedBaseModel): + selectable: bool = pydantic.Field() + """ + Supports choosing what to sync via the hosted picker + """ + + webhooks: bool = pydantic.Field() + """ + Receives change notifications instead of only polling + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/connector_config_value.py b/src/supermemory/types/connector_config_value.py new file mode 100644 index 00000000..a16ec27d --- /dev/null +++ b/src/supermemory/types/connector_config_value.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +ConnectorConfigValue = typing.Union[str, float, bool] diff --git a/src/supermemory/types/connector_deleted.py b/src/supermemory/types/connector_deleted.py new file mode 100644 index 00000000..31c362c2 --- /dev/null +++ b/src/supermemory/types/connector_deleted.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ConnectorDeleted(UncheckedBaseModel): + id: str + deleted: typing.Literal[True] = True + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/connector_error_code.py b/src/supermemory/types/connector_error_code.py new file mode 100644 index 00000000..8d67d409 --- /dev/null +++ b/src/supermemory/types/connector_error_code.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +ConnectorErrorCode = typing.Union[ + typing.Literal["auth_expired", "rate_limited", "plan_required", "provider_unavailable", "internal"], typing.Any +] diff --git a/src/supermemory/types/connector_last_sync.py b/src/supermemory/types/connector_last_sync.py new file mode 100644 index 00000000..677a86dc --- /dev/null +++ b/src/supermemory/types/connector_last_sync.py @@ -0,0 +1,39 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .connector_error_code import ConnectorErrorCode +from .connector_sync_status import ConnectorSyncStatus + + +class ConnectorLastSync(UncheckedBaseModel): + status: ConnectorSyncStatus + error_code: typing_extensions.Annotated[ + typing.Optional[ConnectorErrorCode], FieldMetadata(alias="errorCode"), pydantic.Field(alias="errorCode") + ] = None + error: typing.Optional[str] = pydantic.Field(default=None) + """ + Customer-safe error message + """ + + started_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="startedAt"), pydantic.Field(alias="startedAt") + ] + completed_at: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="completedAt"), pydantic.Field(alias="completedAt") + ] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/connector_list.py b/src/supermemory/types/connector_list.py new file mode 100644 index 00000000..3e8a6b22 --- /dev/null +++ b/src/supermemory/types/connector_list.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .connector import Connector +from .pagination import Pagination + + +class ConnectorList(UncheckedBaseModel): + connectors: typing.List[Connector] + pagination: Pagination = pydantic.Field() + """ + Pagination metadata + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/connector_picker.py b/src/supermemory/types/connector_picker.py new file mode 100644 index 00000000..eabee7df --- /dev/null +++ b/src/supermemory/types/connector_picker.py @@ -0,0 +1,30 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ConnectorPicker(UncheckedBaseModel): + url: str = pydantic.Field() + """ + Send the user here to choose what syncs. Works once, in any browser. + """ + + expires_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="expiresAt"), pydantic.Field(alias="expiresAt") + ] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/connector_provider.py b/src/supermemory/types/connector_provider.py new file mode 100644 index 00000000..2b08328a --- /dev/null +++ b/src/supermemory/types/connector_provider.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +ConnectorProvider = typing.Union[ + typing.Literal["notion", "google-drive", "onedrive", "gmail", "github", "web-crawler", "s3", "granola"], typing.Any +] diff --git a/src/supermemory/types/connector_selection.py b/src/supermemory/types/connector_selection.py new file mode 100644 index 00000000..fc5ab23c --- /dev/null +++ b/src/supermemory/types/connector_selection.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ConnectorSelection(UncheckedBaseModel): + """ + What to sync, as ids grouped by kind. GitHub takes repos, Gmail takes labels, Google Drive takes files and folders. Replaces the current selection. + """ + + repos: typing.Optional[typing.List[str]] = pydantic.Field(default=None) + """ + GitHub repository ids + """ + + labels: typing.Optional[typing.List[str]] = pydantic.Field(default=None) + """ + Gmail label ids + """ + + files: typing.Optional[typing.List[str]] = pydantic.Field(default=None) + """ + Google Drive file ids + """ + + folders: typing.Optional[typing.List[str]] = pydantic.Field(default=None) + """ + Google Drive folder ids + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/connector_selection_item.py b/src/supermemory/types/connector_selection_item.py new file mode 100644 index 00000000..0329e129 --- /dev/null +++ b/src/supermemory/types/connector_selection_item.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ConnectorSelectionItem(UncheckedBaseModel): + id: str + name: str + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/connector_setup.py b/src/supermemory/types/connector_setup.py new file mode 100644 index 00000000..15473289 --- /dev/null +++ b/src/supermemory/types/connector_setup.py @@ -0,0 +1,177 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel, UnionMetadata +from .google_drive_config import GoogleDriveConfig +from .granola_config import GranolaConfig +from .s3config import S3Config +from .web_crawler_config import WebCrawlerConfig + + +class ConnectorSetup_Notion(UncheckedBaseModel): + provider: typing.Literal["notion"] = "notion" + redirect_url: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="redirectUrl"), pydantic.Field(alias="redirectUrl") + ] = None + document_limit: typing_extensions.Annotated[ + typing.Optional[int], FieldMetadata(alias="documentLimit"), pydantic.Field(alias="documentLimit") + ] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow + + +class ConnectorSetup_Gmail(UncheckedBaseModel): + provider: typing.Literal["gmail"] = "gmail" + redirect_url: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="redirectUrl"), pydantic.Field(alias="redirectUrl") + ] = None + document_limit: typing_extensions.Annotated[ + typing.Optional[int], FieldMetadata(alias="documentLimit"), pydantic.Field(alias="documentLimit") + ] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow + + +class ConnectorSetup_Onedrive(UncheckedBaseModel): + provider: typing.Literal["onedrive"] = "onedrive" + redirect_url: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="redirectUrl"), pydantic.Field(alias="redirectUrl") + ] = None + document_limit: typing_extensions.Annotated[ + typing.Optional[int], FieldMetadata(alias="documentLimit"), pydantic.Field(alias="documentLimit") + ] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow + + +class ConnectorSetup_Github(UncheckedBaseModel): + provider: typing.Literal["github"] = "github" + redirect_url: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="redirectUrl"), pydantic.Field(alias="redirectUrl") + ] = None + document_limit: typing_extensions.Annotated[ + typing.Optional[int], FieldMetadata(alias="documentLimit"), pydantic.Field(alias="documentLimit") + ] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow + + +class ConnectorSetup_GoogleDrive(UncheckedBaseModel): + provider: typing.Literal["google-drive"] = "google-drive" + redirect_url: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="redirectUrl"), pydantic.Field(alias="redirectUrl") + ] = None + document_limit: typing_extensions.Annotated[ + typing.Optional[int], FieldMetadata(alias="documentLimit"), pydantic.Field(alias="documentLimit") + ] = None + config: typing.Optional[GoogleDriveConfig] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow + + +class ConnectorSetup_S3(UncheckedBaseModel): + provider: typing.Literal["s3"] = "s3" + config: S3Config + document_limit: typing_extensions.Annotated[ + typing.Optional[int], FieldMetadata(alias="documentLimit"), pydantic.Field(alias="documentLimit") + ] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow + + +class ConnectorSetup_WebCrawler(UncheckedBaseModel): + provider: typing.Literal["web-crawler"] = "web-crawler" + config: WebCrawlerConfig + document_limit: typing_extensions.Annotated[ + typing.Optional[int], FieldMetadata(alias="documentLimit"), pydantic.Field(alias="documentLimit") + ] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow + + +class ConnectorSetup_Granola(UncheckedBaseModel): + provider: typing.Literal["granola"] = "granola" + config: GranolaConfig + document_limit: typing_extensions.Annotated[ + typing.Optional[int], FieldMetadata(alias="documentLimit"), pydantic.Field(alias="documentLimit") + ] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow + + +ConnectorSetup = typing_extensions.Annotated[ + typing.Union[ + ConnectorSetup_Notion, + ConnectorSetup_Gmail, + ConnectorSetup_Onedrive, + ConnectorSetup_Github, + ConnectorSetup_GoogleDrive, + ConnectorSetup_S3, + ConnectorSetup_WebCrawler, + ConnectorSetup_Granola, + ], + UnionMetadata(discriminant="provider"), +] diff --git a/src/supermemory/types/connector_setup_github.py b/src/supermemory/types/connector_setup_github.py new file mode 100644 index 00000000..ae34faa9 --- /dev/null +++ b/src/supermemory/types/connector_setup_github.py @@ -0,0 +1,38 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ConnectorSetupGithub(UncheckedBaseModel): + redirect_url: typing_extensions.Annotated[ + typing.Optional[str], + FieldMetadata(alias="redirectUrl"), + pydantic.Field(alias="redirectUrl", description="Where the user returns after authorizing with the provider"), + ] = None + """ + Where the user returns after authorizing with the provider + """ + + document_limit: typing_extensions.Annotated[ + typing.Optional[int], + FieldMetadata(alias="documentLimit"), + pydantic.Field(alias="documentLimit", description="Maximum documents this connector imports"), + ] = None + """ + Maximum documents this connector imports + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/connector_setup_gmail.py b/src/supermemory/types/connector_setup_gmail.py new file mode 100644 index 00000000..d7222d8f --- /dev/null +++ b/src/supermemory/types/connector_setup_gmail.py @@ -0,0 +1,38 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ConnectorSetupGmail(UncheckedBaseModel): + redirect_url: typing_extensions.Annotated[ + typing.Optional[str], + FieldMetadata(alias="redirectUrl"), + pydantic.Field(alias="redirectUrl", description="Where the user returns after authorizing with the provider"), + ] = None + """ + Where the user returns after authorizing with the provider + """ + + document_limit: typing_extensions.Annotated[ + typing.Optional[int], + FieldMetadata(alias="documentLimit"), + pydantic.Field(alias="documentLimit", description="Maximum documents this connector imports"), + ] = None + """ + Maximum documents this connector imports + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/connector_setup_google_drive.py b/src/supermemory/types/connector_setup_google_drive.py new file mode 100644 index 00000000..731c9772 --- /dev/null +++ b/src/supermemory/types/connector_setup_google_drive.py @@ -0,0 +1,41 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .google_drive_config import GoogleDriveConfig + + +class ConnectorSetupGoogleDrive(UncheckedBaseModel): + redirect_url: typing_extensions.Annotated[ + typing.Optional[str], + FieldMetadata(alias="redirectUrl"), + pydantic.Field(alias="redirectUrl", description="Where the user returns after authorizing with the provider"), + ] = None + """ + Where the user returns after authorizing with the provider + """ + + document_limit: typing_extensions.Annotated[ + typing.Optional[int], + FieldMetadata(alias="documentLimit"), + pydantic.Field(alias="documentLimit", description="Maximum documents this connector imports"), + ] = None + """ + Maximum documents this connector imports + """ + + config: typing.Optional[GoogleDriveConfig] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/connector_setup_granola.py b/src/supermemory/types/connector_setup_granola.py new file mode 100644 index 00000000..8f005ad8 --- /dev/null +++ b/src/supermemory/types/connector_setup_granola.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .granola_config import GranolaConfig + + +class ConnectorSetupGranola(UncheckedBaseModel): + config: GranolaConfig = pydantic.Field() + """ + Granola API key. Stored encrypted and never returned. + """ + + document_limit: typing_extensions.Annotated[ + typing.Optional[int], + FieldMetadata(alias="documentLimit"), + pydantic.Field(alias="documentLimit", description="Maximum documents this connector imports"), + ] = None + """ + Maximum documents this connector imports + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/connector_setup_notion.py b/src/supermemory/types/connector_setup_notion.py new file mode 100644 index 00000000..81a6991c --- /dev/null +++ b/src/supermemory/types/connector_setup_notion.py @@ -0,0 +1,38 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ConnectorSetupNotion(UncheckedBaseModel): + redirect_url: typing_extensions.Annotated[ + typing.Optional[str], + FieldMetadata(alias="redirectUrl"), + pydantic.Field(alias="redirectUrl", description="Where the user returns after authorizing with the provider"), + ] = None + """ + Where the user returns after authorizing with the provider + """ + + document_limit: typing_extensions.Annotated[ + typing.Optional[int], + FieldMetadata(alias="documentLimit"), + pydantic.Field(alias="documentLimit", description="Maximum documents this connector imports"), + ] = None + """ + Maximum documents this connector imports + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/connector_setup_onedrive.py b/src/supermemory/types/connector_setup_onedrive.py new file mode 100644 index 00000000..1d769c1b --- /dev/null +++ b/src/supermemory/types/connector_setup_onedrive.py @@ -0,0 +1,38 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ConnectorSetupOnedrive(UncheckedBaseModel): + redirect_url: typing_extensions.Annotated[ + typing.Optional[str], + FieldMetadata(alias="redirectUrl"), + pydantic.Field(alias="redirectUrl", description="Where the user returns after authorizing with the provider"), + ] = None + """ + Where the user returns after authorizing with the provider + """ + + document_limit: typing_extensions.Annotated[ + typing.Optional[int], + FieldMetadata(alias="documentLimit"), + pydantic.Field(alias="documentLimit", description="Maximum documents this connector imports"), + ] = None + """ + Maximum documents this connector imports + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/connector_setup_result.py b/src/supermemory/types/connector_setup_result.py new file mode 100644 index 00000000..3223089a --- /dev/null +++ b/src/supermemory/types/connector_setup_result.py @@ -0,0 +1,47 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ConnectorSetupResult(UncheckedBaseModel): + id: str = pydantic.Field() + """ + Connector identifier + """ + + auth_url: typing_extensions.Annotated[ + typing.Optional[str], + FieldMetadata(alias="authUrl"), + pydantic.Field( + alias="authUrl", + description="Send the user here to authorize the provider. Null for providers that authenticate with config only.", + ), + ] = None + """ + Send the user here to authorize the provider. Null for providers that authenticate with config only. + """ + + auth_url_expires_at: typing_extensions.Annotated[ + typing.Optional[dt.datetime], + FieldMetadata(alias="authUrlExpiresAt"), + pydantic.Field(alias="authUrlExpiresAt", description="When authUrl stops working"), + ] = None + """ + When authUrl stops working + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/connector_setup_s3.py b/src/supermemory/types/connector_setup_s3.py new file mode 100644 index 00000000..ff263e81 --- /dev/null +++ b/src/supermemory/types/connector_setup_s3.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .s3config import S3Config + + +class ConnectorSetupS3(UncheckedBaseModel): + config: S3Config = pydantic.Field() + """ + S3 bucket to sync. Credentials are stored encrypted and never returned. + """ + + document_limit: typing_extensions.Annotated[ + typing.Optional[int], + FieldMetadata(alias="documentLimit"), + pydantic.Field(alias="documentLimit", description="Maximum documents this connector imports"), + ] = None + """ + Maximum documents this connector imports + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/connector_setup_web_crawler.py b/src/supermemory/types/connector_setup_web_crawler.py new file mode 100644 index 00000000..c549039a --- /dev/null +++ b/src/supermemory/types/connector_setup_web_crawler.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .web_crawler_config import WebCrawlerConfig + + +class ConnectorSetupWebCrawler(UncheckedBaseModel): + config: WebCrawlerConfig = pydantic.Field() + """ + Website to crawl + """ + + document_limit: typing_extensions.Annotated[ + typing.Optional[int], + FieldMetadata(alias="documentLimit"), + pydantic.Field(alias="documentLimit", description="Maximum documents this connector imports"), + ] = None + """ + Maximum documents this connector imports + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/connector_status.py b/src/supermemory/types/connector_status.py new file mode 100644 index 00000000..3ed53c2a --- /dev/null +++ b/src/supermemory/types/connector_status.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +ConnectorStatus = typing.Union[typing.Literal["pending_authorization", "active"], typing.Any] diff --git a/src/supermemory/types/connector_sync.py b/src/supermemory/types/connector_sync.py new file mode 100644 index 00000000..8a069cc6 --- /dev/null +++ b/src/supermemory/types/connector_sync.py @@ -0,0 +1,53 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .connector_error_code import ConnectorErrorCode +from .connector_sync_failure import ConnectorSyncFailure +from .connector_sync_status import ConnectorSyncStatus +from .connector_sync_trigger import ConnectorSyncTrigger + + +class ConnectorSync(UncheckedBaseModel): + status: ConnectorSyncStatus + error_code: typing_extensions.Annotated[ + typing.Optional[ConnectorErrorCode], FieldMetadata(alias="errorCode"), pydantic.Field(alias="errorCode") + ] = None + error: typing.Optional[str] = pydantic.Field(default=None) + """ + Customer-safe error message + """ + + started_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="startedAt"), pydantic.Field(alias="startedAt") + ] + completed_at: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="completedAt"), pydantic.Field(alias="completedAt") + ] = None + id: str + trigger: ConnectorSyncTrigger + items_processed: typing_extensions.Annotated[ + float, FieldMetadata(alias="itemsProcessed"), pydantic.Field(alias="itemsProcessed") + ] + items_failed: typing_extensions.Annotated[ + float, FieldMetadata(alias="itemsFailed"), pydantic.Field(alias="itemsFailed") + ] + failures: typing.List[ConnectorSyncFailure] = pydantic.Field() + """ + Failed items, capped. itemsFailed is the true count. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/connector_sync_failure.py b/src/supermemory/types/connector_sync_failure.py new file mode 100644 index 00000000..056484d9 --- /dev/null +++ b/src/supermemory/types/connector_sync_failure.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ConnectorSyncFailure(UncheckedBaseModel): + document_id: typing_extensions.Annotated[str, FieldMetadata(alias="documentId"), pydantic.Field(alias="documentId")] + title: typing.Optional[str] = None + error_code: typing_extensions.Annotated[str, FieldMetadata(alias="errorCode"), pydantic.Field(alias="errorCode")] + error_message: typing_extensions.Annotated[ + str, FieldMetadata(alias="errorMessage"), pydantic.Field(alias="errorMessage") + ] + failed_at: typing_extensions.Annotated[str, FieldMetadata(alias="failedAt"), pydantic.Field(alias="failedAt")] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/connector_sync_started.py b/src/supermemory/types/connector_sync_started.py new file mode 100644 index 00000000..9381f770 --- /dev/null +++ b/src/supermemory/types/connector_sync_started.py @@ -0,0 +1,25 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ConnectorSyncStarted(UncheckedBaseModel): + id: str = pydantic.Field() + """ + Connector identifier + """ + + status: typing.Literal["queued"] = "queued" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/connector_sync_status.py b/src/supermemory/types/connector_sync_status.py new file mode 100644 index 00000000..20e4882c --- /dev/null +++ b/src/supermemory/types/connector_sync_status.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +ConnectorSyncStatus = typing.Union[typing.Literal["running", "completed", "failed"], typing.Any] diff --git a/src/supermemory/types/connector_sync_trigger.py b/src/supermemory/types/connector_sync_trigger.py new file mode 100644 index 00000000..27a93afe --- /dev/null +++ b/src/supermemory/types/connector_sync_trigger.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +ConnectorSyncTrigger = typing.Union[typing.Literal["event", "cron", "manual"], typing.Any] diff --git a/src/supermemory/types/delete_document_error.py b/src/supermemory/types/delete_document_error.py new file mode 100644 index 00000000..fd073376 --- /dev/null +++ b/src/supermemory/types/delete_document_error.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class DeleteDocumentError(UncheckedBaseModel): + id: str = pydantic.Field() + """ + The requested document identifier + """ + + error: str = pydantic.Field() + """ + Why this document could not be deleted + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/delete_documents_result.py b/src/supermemory/types/delete_documents_result.py new file mode 100644 index 00000000..e3272eac --- /dev/null +++ b/src/supermemory/types/delete_documents_result.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .delete_document_error import DeleteDocumentError + + +class DeleteDocumentsResult(UncheckedBaseModel): + deleted_count: typing_extensions.Annotated[ + int, + FieldMetadata(alias="deletedCount"), + pydantic.Field(alias="deletedCount", description="Number of documents successfully deleted"), + ] + """ + Number of documents successfully deleted + """ + + errors: typing.List[DeleteDocumentError] = pydantic.Field() + """ + Per-document failures; successful deletions are not rolled back + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/document.py b/src/supermemory/types/document.py new file mode 100644 index 00000000..9cd0872f --- /dev/null +++ b/src/supermemory/types/document.py @@ -0,0 +1,70 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .chunk import Chunk +from .document_system import DocumentSystem +from .memory import Memory + + +class Document(UncheckedBaseModel): + """ + Namespace-scoped document. Attachment keys are omitted unless requested and are empty arrays when requested without results. + """ + + id: str = pydantic.Field() + """ + Stable document identifier + """ + + title: typing.Optional[str] = pydantic.Field(default=None) + """ + Extracted or caller-provided document title + """ + + type: str = pydantic.Field() + """ + Detected source or content type + """ + + summary: typing.Optional[str] = pydantic.Field(default=None) + """ + Generated summary of the document, when available + """ + + content: typing.Optional[str] = pydantic.Field(default=None) + """ + Canonical extracted text used for processing and recall + """ + + metadata: typing.Dict[str, typing.Any] = pydantic.Field() + """ + Caller metadata with internal fields removed + """ + + system: DocumentSystem = pydantic.Field() + """ + Processing status, lifecycle timestamps, and storage path maintained by Supermemory + """ + + chunks: typing.Optional[typing.List[Chunk]] = pydantic.Field(default=None) + """ + Ordered document chunks; included only when requested + """ + + memories: typing.Optional[typing.List[Memory]] = pydantic.Field(default=None) + """ + Memories derived from the document; included only when requested + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/document_add_params.py b/src/supermemory/types/document_add_params.py deleted file mode 100644 index 48cca165..00000000 --- a/src/supermemory/types/document_add_params.py +++ /dev/null @@ -1,80 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Dict, Union -from typing_extensions import Literal, Required, Annotated, TypedDict - -from .._types import SequenceNotStr -from .._utils import PropertyInfo - -__all__ = ["DocumentAddParams"] - - -class DocumentAddParams(TypedDict, total=False): - content: Required[str] - """The content to extract and process into a document. - - This can be a URL to a website, a PDF, an image, or a video. - """ - - container_tag: Annotated[str, PropertyInfo(alias="containerTag")] - """Optional tag this document should be containerized by. - - Max 100 characters, alphanumeric with hyphens, underscores, and dots only. - """ - - container_tags: Annotated[SequenceNotStr[str], PropertyInfo(alias="containerTags")] - - custom_id: Annotated[str, PropertyInfo(alias="customId")] - """Optional custom ID of the document. - - Max 100 characters, alphanumeric with hyphens, underscores, and dots only. - """ - - document_date: Annotated[str, PropertyInfo(alias="documentDate")] - """When this document's content is from, as opposed to when it was uploaded. - - Accepts YYYY-MM-DD or a full ISO 8601 timestamp. Memory extraction resolves - relative dates against this instead of the ingestion time, and documents in a - batch are processed oldest-first so newer facts correctly supersede older ones. - Set this whenever you backfill historical content. - """ - - dreaming: Literal["instant", "dynamic"] - """Processing mode. - - "dynamic" (default) groups related documents together so memories form from - coherent, logical units rather than one isolated entry at a time. "instant" - processes each document on its own right away, and bills one extra operation per - document. - """ - - entity_context: Annotated[str, PropertyInfo(alias="entityContext")] - """Optional entity context for this container tag. - - Max 1500 characters. Used during document processing to guide memory extraction. - """ - - filepath: str - """Optional file path for the document. - - Used by supermemoryfs to store the full path of the file. - """ - - filter_by_metadata: Annotated[ - Dict[str, Union[str, float, bool, SequenceNotStr[str]]], PropertyInfo(alias="filterByMetadata") - ] - """ - Optional metadata filter to apply when pulling related memories and profile - during ingestion. Only memories matching these filters will be used as context. - """ - - metadata: Dict[str, Union[str, float, bool, SequenceNotStr[str]]] - """Optional metadata for the document.""" - - task_type: Annotated[Literal["memory", "superrag"], PropertyInfo(alias="taskType")] - """ - Task type: "memory" (default) for full context layer with SuperRAG built in, - "superrag" for managed RAG as a service. - """ diff --git a/src/supermemory/types/document_add_response.py b/src/supermemory/types/document_add_response.py deleted file mode 100644 index e6a5f01d..00000000 --- a/src/supermemory/types/document_add_response.py +++ /dev/null @@ -1,13 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from .._models import BaseModel - -__all__ = ["DocumentAddResponse"] - - -class DocumentAddResponse(BaseModel): - id: str - """Unique identifier of the document""" - - status: str - """Status of the document""" diff --git a/src/supermemory/types/document_attachment.py b/src/supermemory/types/document_attachment.py new file mode 100644 index 00000000..b691cd9c --- /dev/null +++ b/src/supermemory/types/document_attachment.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +DocumentAttachment = typing.Union[typing.Literal["chunks", "memories"], typing.Any] diff --git a/src/supermemory/types/document_batch_add_params.py b/src/supermemory/types/document_batch_add_params.py deleted file mode 100644 index 6afaeb65..00000000 --- a/src/supermemory/types/document_batch_add_params.py +++ /dev/null @@ -1,178 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Dict, Union, Iterable -from typing_extensions import Literal, Required, Annotated, TypedDict - -from .._types import SequenceNotStr -from .._utils import PropertyInfo - -__all__ = ["DocumentBatchAddParams", "DocumentsUnionMember0"] - - -class DocumentBatchAddParams(TypedDict, total=False): - documents: Required[Union[Iterable[DocumentsUnionMember0], SequenceNotStr[str]]] - - 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. - """ - - 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. - """ - - content: None - - document_date: Annotated[str, PropertyInfo(alias="documentDate")] - """When this document's content is from, as opposed to when it was uploaded. - - Accepts YYYY-MM-DD or a full ISO 8601 timestamp. Memory extraction resolves - relative dates ('yesterday', 'last quarter') against this instead of the - ingestion time, and documents in a batch are processed oldest-first so newer - facts correctly supersede older ones. Set this whenever you backfill historical - content. - """ - - dreaming: Literal["instant", "dynamic"] - """Processing mode. - - "dynamic" (default) groups related documents together so memories form from - coherent, logical units rather than one isolated entry at a time. "instant" - processes each document on its own right away, and bills one extra operation per - document. - """ - - entity_context: Annotated[str, PropertyInfo(alias="entityContext")] - """Optional entity context for this container tag. - - Max 1500 characters. Used during document processing to guide memory extraction. - """ - - filepath: str - """Optional file path for the document (e.g., '/documents/reports/file.pdf'). - - Used by supermemoryfs to map documents to filesystem paths. - """ - - filter_by_metadata: Annotated[ - Dict[str, Union[str, float, bool, SequenceNotStr[str]]], PropertyInfo(alias="filterByMetadata") - ] - """ - Optional metadata filter scoping which existing memories are pulled as context - during ingestion. Scalar values match exactly (AND across keys); array values - match ANY (OR within key). Only memories whose source documents match this - filter are used as context. - """ - - 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. - """ - - task_type: Annotated[Literal["memory", "superrag"], PropertyInfo(alias="taskType")] - """ - Task type: "memory" (default) for full context layer with SuperRAG built in, - "superrag" for managed RAG as a service. - """ - - -class DocumentsUnionMember0(TypedDict, total=False): - content: Required[str] - """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. - """ - - 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. - """ - - document_date: Annotated[str, PropertyInfo(alias="documentDate")] - """When this document's content is from, as opposed to when it was uploaded. - - Accepts YYYY-MM-DD or a full ISO 8601 timestamp. Memory extraction resolves - relative dates ('yesterday', 'last quarter') against this instead of the - ingestion time, and documents in a batch are processed oldest-first so newer - facts correctly supersede older ones. Set this whenever you backfill historical - content. - """ - - dreaming: Literal["instant", "dynamic"] - """Processing mode. - - "dynamic" (default) groups related documents together so memories form from - coherent, logical units rather than one isolated entry at a time. "instant" - processes each document on its own right away, and bills one extra operation per - document. - """ - - entity_context: Annotated[str, PropertyInfo(alias="entityContext")] - """Optional entity context for this container tag. - - Max 1500 characters. Used during document processing to guide memory extraction. - """ - - filepath: str - """Optional file path for the document (e.g., '/documents/reports/file.pdf'). - - Used by supermemoryfs to map documents to filesystem paths. - """ - - filter_by_metadata: Annotated[ - Dict[str, Union[str, float, bool, SequenceNotStr[str]]], PropertyInfo(alias="filterByMetadata") - ] - """ - Optional metadata filter scoping which existing memories are pulled as context - during ingestion. Scalar values match exactly (AND across keys); array values - match ANY (OR within key). Only memories whose source documents match this - filter are used as context. - """ - - 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. - """ - - task_type: Annotated[Literal["memory", "superrag"], PropertyInfo(alias="taskType")] - """ - Task type: "memory" (default) for full context layer with SuperRAG built in, - "superrag" for managed RAG as a service. - """ diff --git a/src/supermemory/types/document_batch_add_response.py b/src/supermemory/types/document_batch_add_response.py deleted file mode 100644 index b4a11bcd..00000000 --- a/src/supermemory/types/document_batch_add_response.py +++ /dev/null @@ -1,40 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List, Optional - -from .._models import BaseModel - -__all__ = ["DocumentBatchAddResponse", "Result"] - - -class Result(BaseModel): - id: str - """Unique identifier of the document (empty string for failed items)""" - - status: str - """Status of the document (e.g. 'done', 'queued', 'error')""" - - details: Optional[str] = None - """Additional error details when status is 'error'""" - - error: Optional[str] = None - """Error message when status is 'error'""" - - url: Optional[str] = None - """URL of the failed item, when it had one. - - Only present on failed items, where it may be the only way to tell which input - the error belongs to (the id falls back to 'unknown' when the input had neither - an id nor a customId). - """ - - -class DocumentBatchAddResponse(BaseModel): - failed: float - """Count of documents that failed to add""" - - results: List[Result] - """Array of results for each document in the batch""" - - success: float - """Count of documents successfully added""" diff --git a/src/supermemory/types/document_delete_bulk_params.py b/src/supermemory/types/document_delete_bulk_params.py deleted file mode 100644 index 3dd221a2..00000000 --- a/src/supermemory/types/document_delete_bulk_params.py +++ /dev/null @@ -1,24 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Annotated, TypedDict - -from .._types import SequenceNotStr -from .._utils import PropertyInfo - -__all__ = ["DocumentDeleteBulkParams"] - - -class DocumentDeleteBulkParams(TypedDict, total=False): - container_tags: Annotated[SequenceNotStr[str], PropertyInfo(alias="containerTags")] - """Array of container tags - all documents in these containers will be deleted""" - - filepath: str - """Delete documents matching this filepath. - - Exact match for full paths, prefix match if ending with / - """ - - ids: SequenceNotStr[str] - """Array of document IDs to delete (max 100 at once)""" diff --git a/src/supermemory/types/document_delete_bulk_response.py b/src/supermemory/types/document_delete_bulk_response.py deleted file mode 100644 index 8af4af02..00000000 --- a/src/supermemory/types/document_delete_bulk_response.py +++ /dev/null @@ -1,37 +0,0 @@ -# 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__ = ["DocumentDeleteBulkResponse", "Error"] - - -class Error(BaseModel): - id: str - - error: str - - -class DocumentDeleteBulkResponse(BaseModel): - """Response for bulk document deletion""" - - deleted_count: float = FieldInfo(alias="deletedCount") - """Number of documents successfully deleted""" - - success: bool - """Whether the bulk deletion was successful""" - - container_tags: Optional[List[str]] = FieldInfo(alias="containerTags", default=None) - """ - Container tags that were processed (only applicable when deleting by container - tags) - """ - - errors: Optional[List[Error]] = None - """ - Array of errors for documents that couldn't be deleted (only applicable when - deleting by IDs) - """ diff --git a/src/supermemory/types/document_get_response.py b/src/supermemory/types/document_get_response.py deleted file mode 100644 index d213d301..00000000 --- a/src/supermemory/types/document_get_response.py +++ /dev/null @@ -1,153 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict, List, Union, Optional -from datetime import datetime -from typing_extensions import Literal - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["DocumentGetResponse", "Memory"] - - -class Memory(BaseModel): - id: str - - created_at: str = FieldInfo(alias="createdAt") - """Creation timestamp""" - - is_forgotten: bool = FieldInfo(alias="isForgotten") - - is_inference: bool = FieldInfo(alias="isInference") - - is_latest: bool = FieldInfo(alias="isLatest") - - is_static: bool = FieldInfo(alias="isStatic") - - memory: str - - metadata: Union[str, float, bool, Dict[str, object], List[object], None] = None - - parent_memory_id: Optional[str] = FieldInfo(alias="parentMemoryId", default=None) - - root_memory_id: Optional[str] = FieldInfo(alias="rootMemoryId", default=None) - - source_count: int = FieldInfo(alias="sourceCount") - - updated_at: Optional[str] = FieldInfo(alias="updatedAt", default=None) - """Last update timestamp""" - - version: int - - -class DocumentGetResponse(BaseModel): - id: str - """Unique identifier of the document.""" - - active_content_update_id: Optional[str] = FieldInfo(alias="activeContentUpdateId", default=None) - - connection_id: Optional[str] = FieldInfo(alias="connectionId", default=None) - """Optional ID of connection the document was created from. - - This is useful for identifying the source of the document. - """ - - content: Optional[str] = None - """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. - """ - - created_at: str = FieldInfo(alias="createdAt") - """Creation timestamp""" - - custom_id: Optional[str] = FieldInfo(alias="customId", default=None) - """Optional custom ID of the document. - - This could be an ID from your database that will uniquely identify this - document. - """ - - dreaming_status: Literal["dreaming", "done"] = FieldInfo(alias="dreamingStatus") - - filepath: Optional[str] = None - - latest_revision: int = FieldInfo(alias="latestRevision") - - metadata: Union[str, float, bool, Dict[str, object], List[object], None] = None - """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. - """ - - og_image: Optional[str] = FieldInfo(alias="ogImage", default=None) - - raw: object - """Raw content of the document""" - - source: Optional[str] = None - """Source of the document""" - - status: Literal["unknown", "queued", "extracting", "chunking", "embedding", "indexing", "done", "failed"] - """Status of the document""" - - summary: Optional[str] = None - """Summary of the document content""" - - task_type: Literal["memory", "superrag"] = FieldInfo(alias="taskType") - """ - Task type: "memory" (default) for full context layer with SuperRAG built in, - "superrag" for managed RAG as a service. - """ - - title: Optional[str] = None - """Title of the document""" - - tombstoned_at: Optional[datetime] = FieldInfo(alias="tombstonedAt", default=None) - - type: Literal[ - "text", - "pdf", - "tweet", - "google_doc", - "google_slide", - "google_sheet", - "image", - "video", - "audio", - "notion_doc", - "webpage", - "onedrive", - "github_markdown", - "granola", - ] - """Type of the document""" - - updated_at: str = FieldInfo(alias="updatedAt") - """Last update timestamp""" - - container_tags: Optional[List[str]] = FieldInfo(alias="containerTags", default=None) - """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. - """ - - memories: Optional[List[Memory]] = None - """Memories extracted from this document. - - Omitted when the document has no memories (e.g. still processing). - """ - - url: Optional[str] = None - """URL of the document""" diff --git a/src/supermemory/types/document_input.py b/src/supermemory/types/document_input.py new file mode 100644 index 00000000..4dbcfb60 --- /dev/null +++ b/src/supermemory/types/document_input.py @@ -0,0 +1,58 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .metadata_value import MetadataValue + + +class DocumentInput(UncheckedBaseModel): + content: str = pydantic.Field() + """ + The content to process. This may be plaintext or a URL to supported rich content. + """ + + id: typing.Optional[str] = pydantic.Field(default=None) + """ + An optional caller-defined document ID + """ + + supporting_context: typing_extensions.Annotated[ + typing.Optional[str], + FieldMetadata(alias="supportingContext"), + pydantic.Field( + alias="supportingContext", + description="Context used to guide memory extraction within this namespace. Max 1500 characters.", + ), + ] = None + """ + Context used to guide memory extraction within this namespace. Max 1500 characters. + """ + + metadata: typing.Optional[typing.Dict[str, MetadataValue]] = pydantic.Field(default=None) + """ + Arbitrary metadata attached to the document and derived data + """ + + group: typing.Optional[typing.Dict[str, MetadataValue]] = pydantic.Field(default=None) + """ + Metadata values that isolate related context within the namespace + """ + + date: typing.Optional[str] = pydantic.Field(default=None) + """ + When the source content is from, in ISO 8601 format + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/document_list_item.py b/src/supermemory/types/document_list_item.py new file mode 100644 index 00000000..56346b4d --- /dev/null +++ b/src/supermemory/types/document_list_item.py @@ -0,0 +1,58 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .document_system import DocumentSystem + + +class DocumentListItem(UncheckedBaseModel): + """ + A document in this namespace without its content + """ + + id: str = pydantic.Field() + """ + Stable document identifier + """ + + title: typing.Optional[str] = pydantic.Field(default=None) + """ + Extracted or caller-provided document title + """ + + type: str = pydantic.Field() + """ + Detected source or content type + """ + + summary: typing.Optional[str] = pydantic.Field(default=None) + """ + Generated summary of the document, when available + """ + + metadata: typing.Dict[str, typing.Any] = pydantic.Field() + """ + Public metadata attached to the document + """ + + url: typing.Optional[str] = pydantic.Field(default=None) + """ + Original source URL, when the document was ingested from the web + """ + + system: DocumentSystem = pydantic.Field() + """ + Processing status, lifecycle timestamps, and storage path maintained by Supermemory + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/document_list_params.py b/src/supermemory/types/document_list_params.py deleted file mode 100644 index ff720edc..00000000 --- a/src/supermemory/types/document_list_params.py +++ /dev/null @@ -1,139 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Union, Iterable -from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict - -from .._types import SequenceNotStr -from .._utils import PropertyInfo - -__all__ = [ - "DocumentListParams", - "Filters", - "FiltersOr", - "FiltersOrOr", - "FiltersOrOrFilterCondition", - "FiltersOrOrOr", - "FiltersOrOrAnd", - "FiltersAnd", - "FiltersAndAnd", - "FiltersAndAndFilterCondition", - "FiltersAndAndOr", - "FiltersAndAndAnd", -] - - -class DocumentListParams(TypedDict, total=False): - container_tags: Annotated[SequenceNotStr[str], PropertyInfo(alias="containerTags")] - """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. - """ - - filepath: str - """Filter documents by filepath. - - Exact match for full paths, prefix match if ending with / - """ - - filters: Filters - """Optional filters to apply to the search. Can be a JSON string or Query object.""" - - include_content: Annotated[bool, PropertyInfo(alias="includeContent")] - """Whether to include the content field in the response. - - Warning: This can make responses significantly larger. - """ - - limit: Union[str, float] - """Number of items per page""" - - order: Literal["asc", "desc"] - """Sort order""" - - page: Union[str, float] - """Page number to fetch""" - - sort: Literal["createdAt", "updatedAt"] - """Field to sort by""" - - -class FiltersOrOrFilterCondition(TypedDict, total=False): - """ - A single filter condition based on metadata, numeric values, array contents, or string matching - """ - - key: Required[str] - - value: Required[str] - - filter_type: Annotated[ - Literal["metadata", "numeric", "array_contains", "string_contains"], PropertyInfo(alias="filterType") - ] - - ignore_case: Annotated[Union[bool, Literal["true", "false"]], PropertyInfo(alias="ignoreCase")] - - negate: Union[bool, Literal["true", "false"]] - - numeric_operator: Annotated[Literal[">", "<", ">=", "<=", "="], PropertyInfo(alias="numericOperator")] - - -class FiltersOrOrOr(TypedDict, total=False): - or_: Required[Annotated[Iterable[object], PropertyInfo(alias="OR")]] - """OR: Array of conditions or nested expressions""" - - -class FiltersOrOrAnd(TypedDict, total=False): - and_: Required[Annotated[Iterable[object], PropertyInfo(alias="AND")]] - """AND: Array of conditions or nested expressions""" - - -FiltersOrOr: TypeAlias = Union[FiltersOrOrFilterCondition, FiltersOrOrOr, FiltersOrOrAnd] - - -class FiltersOr(TypedDict, total=False): - or_: Required[Annotated[Iterable[FiltersOrOr], PropertyInfo(alias="OR")]] - """Array of OR filter expressions""" - - -class FiltersAndAndFilterCondition(TypedDict, total=False): - """ - A single filter condition based on metadata, numeric values, array contents, or string matching - """ - - key: Required[str] - - value: Required[str] - - filter_type: Annotated[ - Literal["metadata", "numeric", "array_contains", "string_contains"], PropertyInfo(alias="filterType") - ] - - ignore_case: Annotated[Union[bool, Literal["true", "false"]], PropertyInfo(alias="ignoreCase")] - - negate: Union[bool, Literal["true", "false"]] - - numeric_operator: Annotated[Literal[">", "<", ">=", "<=", "="], PropertyInfo(alias="numericOperator")] - - -class FiltersAndAndOr(TypedDict, total=False): - or_: Required[Annotated[Iterable[object], PropertyInfo(alias="OR")]] - """OR: Array of conditions or nested expressions""" - - -class FiltersAndAndAnd(TypedDict, total=False): - and_: Required[Annotated[Iterable[object], PropertyInfo(alias="AND")]] - """AND: Array of conditions or nested expressions""" - - -FiltersAndAnd: TypeAlias = Union[FiltersAndAndFilterCondition, FiltersAndAndOr, FiltersAndAndAnd] - - -class FiltersAnd(TypedDict, total=False): - and_: Required[Annotated[Iterable[FiltersAndAnd], PropertyInfo(alias="AND")]] - """Array of AND filter expressions""" - - -Filters: TypeAlias = Union[FiltersOr, FiltersAnd] diff --git a/src/supermemory/types/document_list_processing_params.py b/src/supermemory/types/document_list_processing_params.py deleted file mode 100644 index fec66a22..00000000 --- a/src/supermemory/types/document_list_processing_params.py +++ /dev/null @@ -1,28 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Union -from typing_extensions import Literal, Annotated, TypedDict - -from .._utils import PropertyInfo - -__all__ = ["DocumentListProcessingParams"] - - -class DocumentListProcessingParams(TypedDict, total=False): - container_tags: Annotated[str, PropertyInfo(alias="containerTags")] - """Comma-separated container tags to filter by""" - - limit: Union[str, float] - """Number of items per page. Used with `view=all`.""" - - page: Union[str, float] - """Page number to fetch. Used with `view=all`.""" - - view: Literal["active", "pending", "all"] - """`active` returns in-flight documents updated in the last 4 hours. - - `pending` returns every document that is not done or failed, with no time - cutoff. `all` also includes failed documents, paginated. - """ diff --git a/src/supermemory/types/document_list_processing_response.py b/src/supermemory/types/document_list_processing_response.py deleted file mode 100644 index ec321497..00000000 --- a/src/supermemory/types/document_list_processing_response.py +++ /dev/null @@ -1,110 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict, List, Union, Optional -from typing_extensions import Literal - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["DocumentListProcessingResponse", "Document", "Pagination"] - - -class Document(BaseModel): - id: str - """Unique identifier of the document.""" - - connection_id: Optional[str] = FieldInfo(alias="connectionId", default=None) - """Optional ID of connection the document was created from. - - This is useful for identifying the source of the document. - """ - - created_at: str = FieldInfo(alias="createdAt") - """Creation timestamp""" - - custom_id: Optional[str] = FieldInfo(alias="customId", default=None) - """Optional custom ID of the document. - - This could be an ID from your database that will uniquely identify this - document. - """ - - metadata: Union[str, float, bool, Dict[str, object], List[object], None] = None - """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. - """ - - status: Literal["unknown", "queued", "extracting", "chunking", "embedding", "indexing", "done", "failed"] - """Status of the document""" - - summary: Optional[str] = None - """Summary of the document content""" - - title: Optional[str] = None - """Title of the document""" - - type: Literal[ - "text", - "pdf", - "tweet", - "google_doc", - "google_slide", - "google_sheet", - "image", - "video", - "audio", - "notion_doc", - "webpage", - "onedrive", - "github_markdown", - "granola", - ] - """Type of the document""" - - updated_at: str = FieldInfo(alias="updatedAt") - """Last update timestamp""" - - container_tags: Optional[List[str]] = FieldInfo(alias="containerTags", default=None) - """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. - """ - - error_code: Optional[str] = FieldInfo(alias="errorCode", default=None) - """Stable error code when processing failed, if available""" - - error_message: Optional[str] = FieldInfo(alias="errorMessage", default=None) - """Human-readable processing error, if available""" - - stuck: Optional[bool] = None - """True when the document is not done/failed and has not been updated for 4 hours""" - - -class Pagination(BaseModel): - """Present when `view=all`""" - - current_page: float = FieldInfo(alias="currentPage") - - total_items: float = FieldInfo(alias="totalItems") - - total_pages: float = FieldInfo(alias="totalPages") - - limit: Optional[float] = None - - -class DocumentListProcessingResponse(BaseModel): - """List of documents currently being processed""" - - documents: List[Document] - - total_count: float = FieldInfo(alias="totalCount") - """Total number of processing documents""" - - pagination: Optional[Pagination] = None - """Present when `view=all`""" diff --git a/src/supermemory/types/document_list_response.py b/src/supermemory/types/document_list_response.py deleted file mode 100644 index 3c9f7801..00000000 --- a/src/supermemory/types/document_list_response.py +++ /dev/null @@ -1,106 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict, List, Union, Optional -from typing_extensions import Literal - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["DocumentListResponse", "Memory", "Pagination"] - - -class Memory(BaseModel): - id: str - """Unique identifier of the document.""" - - connection_id: Optional[str] = FieldInfo(alias="connectionId", default=None) - """Optional ID of connection the document was created from. - - This is useful for identifying the source of the document. - """ - - created_at: str = FieldInfo(alias="createdAt") - """Creation timestamp""" - - custom_id: Optional[str] = FieldInfo(alias="customId", default=None) - """Optional custom ID of the document. - - This could be an ID from your database that will uniquely identify this - document. - """ - - filepath: Optional[str] = None - - metadata: Union[str, float, bool, Dict[str, object], List[object], None] = None - """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. - """ - - status: Literal["unknown", "queued", "extracting", "chunking", "embedding", "indexing", "done", "failed"] - """Status of the document""" - - summary: Optional[str] = None - """Summary of the document content""" - - title: Optional[str] = None - """Title of the document""" - - type: Literal[ - "text", - "pdf", - "tweet", - "google_doc", - "google_slide", - "google_sheet", - "image", - "video", - "audio", - "notion_doc", - "webpage", - "onedrive", - "github_markdown", - "granola", - ] - """Type of the document""" - - updated_at: str = FieldInfo(alias="updatedAt") - """Last update timestamp""" - - container_tags: Optional[List[str]] = FieldInfo(alias="containerTags", default=None) - """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. - """ - - content: Optional[str] = None - """Content of the document (only included when includeContent=true)""" - - url: Optional[str] = None - """URL of the document""" - - -class Pagination(BaseModel): - """Pagination metadata""" - - current_page: float = FieldInfo(alias="currentPage") - - total_items: float = FieldInfo(alias="totalItems") - - total_pages: float = FieldInfo(alias="totalPages") - - limit: Optional[float] = None - - -class DocumentListResponse(BaseModel): - """List of documents""" - - memories: List[Memory] - - pagination: Pagination - """Pagination metadata""" diff --git a/src/supermemory/types/document_ref.py b/src/supermemory/types/document_ref.py new file mode 100644 index 00000000..6df71cc7 --- /dev/null +++ b/src/supermemory/types/document_ref.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class DocumentRef(UncheckedBaseModel): + id: str = pydantic.Field() + """ + Unique identifier of the document + """ + + status: str = pydantic.Field() + """ + Status of the document + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/document_system.py b/src/supermemory/types/document_system.py new file mode 100644 index 00000000..84141d08 --- /dev/null +++ b/src/supermemory/types/document_system.py @@ -0,0 +1,48 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class DocumentSystem(UncheckedBaseModel): + """ + Processing status, lifecycle timestamps, and storage path maintained by Supermemory + """ + + status: str = pydantic.Field() + """ + Current extraction and memory-processing status + """ + + created_at: typing_extensions.Annotated[ + dt.datetime, + FieldMetadata(alias="createdAt"), + pydantic.Field(alias="createdAt", description="ISO 8601 timestamp when the document was created"), + ] + """ + ISO 8601 timestamp when the document was created + """ + + updated_at: typing_extensions.Annotated[ + dt.datetime, + FieldMetadata(alias="updatedAt"), + pydantic.Field(alias="updatedAt", description="ISO 8601 timestamp of the latest document update"), + ] + """ + ISO 8601 timestamp of the latest document update + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/document_update_params.py b/src/supermemory/types/document_update_params.py deleted file mode 100644 index be9466a0..00000000 --- a/src/supermemory/types/document_update_params.py +++ /dev/null @@ -1,87 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Dict, Union -from typing_extensions import Literal, Annotated, TypedDict - -from .._types import SequenceNotStr -from .._utils import PropertyInfo - -__all__ = ["DocumentUpdateParams"] - - -class DocumentUpdateParams(TypedDict, total=False): - 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. - """ - - 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. - """ - - content: str - """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. - """ - - 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. - """ - - document_date: Annotated[str, PropertyInfo(alias="documentDate")] - """When this document's content is from, as opposed to when it was uploaded. - - Accepts YYYY-MM-DD or a full ISO 8601 timestamp. Memory extraction resolves - relative dates ('yesterday', 'last quarter') against this instead of the - ingestion time, and documents in a batch are processed oldest-first so newer - facts correctly supersede older ones. Set this whenever you backfill historical - content. - """ - - filepath: str - """Optional file path for the document (e.g., '/documents/reports/file.pdf'). - - Used by supermemoryfs to map documents to filesystem paths. - """ - - filter_by_metadata: Annotated[ - Dict[str, Union[str, float, bool, SequenceNotStr[str]]], PropertyInfo(alias="filterByMetadata") - ] - """ - Optional metadata filter scoping which existing memories are pulled as context - during ingestion. Scalar values match exactly (AND across keys); array values - match ANY (OR within key). Only memories whose source documents match this - filter are used as context. - """ - - 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. - """ - - task_type: Annotated[Literal["memory", "superrag"], PropertyInfo(alias="taskType")] - """ - Task type: "memory" (default) for full context layer with SuperRAG built in, - "superrag" for managed RAG as a service. - """ diff --git a/src/supermemory/types/document_update_response.py b/src/supermemory/types/document_update_response.py deleted file mode 100644 index 464d9c45..00000000 --- a/src/supermemory/types/document_update_response.py +++ /dev/null @@ -1,13 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from .._models import BaseModel - -__all__ = ["DocumentUpdateResponse"] - - -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_params.py b/src/supermemory/types/document_upload_file_params.py deleted file mode 100644 index c11dd767..00000000 --- a/src/supermemory/types/document_upload_file_params.py +++ /dev/null @@ -1,95 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Literal, Required, Annotated, TypedDict - -from .._types import FileTypes -from .._utils import PropertyInfo - -__all__ = ["DocumentUploadFileParams"] - - -class DocumentUploadFileParams(TypedDict, total=False): - file: Required[FileTypes] - """File to upload and process""" - - container_tag: Annotated[str, PropertyInfo(alias="containerTag")] - """Optional container tag (e.g., 'user_123'). Use this for a single tag.""" - - container_tags: Annotated[str, PropertyInfo(alias="containerTags")] - """Optional container tags. - - Can be either a JSON string of an array (e.g., '["user_123", "project_123"]') or - a single string (e.g., 'user_123'). Single strings will be automatically - converted to an array. - """ - - custom_id: Annotated[str, PropertyInfo(alias="customId")] - """Optional custom ID of the document. - - Max 100 characters, alphanumeric with hyphens, underscores, and colons only. - """ - - dreaming: Literal["instant", "dynamic"] - """Processing mode. - - "dynamic" (default) groups related documents together so memories form from - coherent, logical units rather than one isolated entry at a time. "instant" - processes each document on its own right away, and bills one extra operation per - document. - """ - - entity_context: Annotated[str, PropertyInfo(alias="entityContext")] - """Optional entity context for this container tag. - - Max 1500 characters. Used during document processing to guide memory extraction. - """ - - filepath: str - """Optional file path for the uploaded file (e.g., '/documents/reports/file.pdf'). - - Used by supermemoryfs to map documents to filesystem paths. - """ - - file_type: Annotated[str, PropertyInfo(alias="fileType")] - """Optional file type override to force specific processing behavior. - - Valid values: text, pdf, tweet, google_doc, google_slide, google_sheet, image, - video, notion_doc, webpage, onedrive - """ - - filter_by_metadata: Annotated[str, PropertyInfo(alias="filterByMetadata")] - """Optional metadata filter as a JSON string. - - Scopes which existing memories are pulled as context during ingestion. Scalar - values match exactly (AND across keys); array values match ANY (OR within key). - """ - - metadata: str - """Optional metadata for the document as a JSON string. - - This is used to store additional information about the document. Keys must be - strings and values can be strings, numbers, or booleans. - """ - - mime_type: Annotated[str, PropertyInfo(alias="mimeType")] - """Required when fileType is 'image' or 'video'. - - Specifies the exact MIME type to use (e.g., 'image/png', 'image/jpeg', - 'video/mp4', 'video/webm') - """ - - task_type: Annotated[Literal["memory", "superrag"], PropertyInfo(alias="taskType")] - """ - Task type: "memory" (default) for full context layer with SuperRAG built in, - "superrag" for managed RAG as a service. - """ - - use_advanced_processing: Annotated[str, PropertyInfo(alias="useAdvancedProcessing")] - """DEPRECATED: This field is no longer used. - - Advanced PDF processing is now automatic with our hybrid Mistral OCR + Gemini - pipeline. This parameter will be accepted but ignored for backwards - compatibility. - """ diff --git a/src/supermemory/types/document_upload_file_response.py b/src/supermemory/types/document_upload_file_response.py deleted file mode 100644 index 218a07d4..00000000 --- a/src/supermemory/types/document_upload_file_response.py +++ /dev/null @@ -1,13 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from .._models import BaseModel - -__all__ = ["DocumentUploadFileResponse"] - - -class DocumentUploadFileResponse(BaseModel): - id: str - """Unique identifier of the document""" - - status: str - """Status of the document""" diff --git a/src/supermemory/types/dreaming_mode.py b/src/supermemory/types/dreaming_mode.py new file mode 100644 index 00000000..c591abab --- /dev/null +++ b/src/supermemory/types/dreaming_mode.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +DreamingMode = typing.Union[typing.Literal["dynamic", "instant"], typing.Any] diff --git a/src/supermemory/types/error_response.py b/src/supermemory/types/error_response.py new file mode 100644 index 00000000..d46b711b --- /dev/null +++ b/src/supermemory/types/error_response.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ErrorResponse(UncheckedBaseModel): + error: str = pydantic.Field() + """ + Error message + """ + + details: typing.Optional[str] = pydantic.Field(default=None) + """ + Additional error details + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/file_type.py b/src/supermemory/types/file_type.py new file mode 100644 index 00000000..4decdf6c --- /dev/null +++ b/src/supermemory/types/file_type.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +FileType = typing.Union[typing.Literal["text", "pdf", "image", "video", "audio"], typing.Any] diff --git a/src/supermemory/types/file_upload_result.py b/src/supermemory/types/file_upload_result.py new file mode 100644 index 00000000..997e6be7 --- /dev/null +++ b/src/supermemory/types/file_upload_result.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class FileUploadResult(UncheckedBaseModel): + id: str = pydantic.Field() + """ + Document identifier to poll or retrieve + """ + + status: typing.Literal["processing"] = pydantic.Field(default="processing") + """ + Confirms the file was accepted for asynchronous processing + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/filter_and.py b/src/supermemory/types/filter_and.py new file mode 100644 index 00000000..11695cfc --- /dev/null +++ b/src/supermemory/types/filter_and.py @@ -0,0 +1,36 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, update_forward_refs +from ..core.unchecked_base_model import UncheckedBaseModel + + +class FilterAnd(UncheckedBaseModel): + operator: typing.Literal["and"] = pydantic.Field(default="and") + """ + Require every nested filter expression to match + """ + + operands: typing.List["FilterExpression"] = pydantic.Field() + """ + Filter expressions combined with logical AND + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow + + +from .filter_expression import FilterExpression # noqa: E402, I001 +from .filter_or import FilterOr # noqa: E402, I001 + +update_forward_refs(FilterAnd, FilterExpression=FilterExpression, FilterOr=FilterOr) diff --git a/src/supermemory/types/filter_array_contains.py b/src/supermemory/types/filter_array_contains.py new file mode 100644 index 00000000..925173f9 --- /dev/null +++ b/src/supermemory/types/filter_array_contains.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .filter_array_contains_operator import FilterArrayContainsOperator + + +class FilterArrayContains(UncheckedBaseModel): + field: str = pydantic.Field() + """ + Metadata key to evaluate, matched literally. A period is part of the key name, not a path into nested metadata. + """ + + operator: FilterArrayContainsOperator = pydantic.Field() + """ + Require or exclude an exact array member + """ + + value: str = pydantic.Field() + """ + Array member to look for + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/filter_array_contains_operator.py b/src/supermemory/types/filter_array_contains_operator.py new file mode 100644 index 00000000..ac7df912 --- /dev/null +++ b/src/supermemory/types/filter_array_contains_operator.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +FilterArrayContainsOperator = typing.Union[typing.Literal["arrayContains", "arrayNotContains"], typing.Any] diff --git a/src/supermemory/types/filter_expression.py b/src/supermemory/types/filter_expression.py new file mode 100644 index 00000000..8e82fc33 --- /dev/null +++ b/src/supermemory/types/filter_expression.py @@ -0,0 +1,12 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +from .filter_predicate import FilterPredicate + +if typing.TYPE_CHECKING: + from .filter_and import FilterAnd + from .filter_or import FilterOr +FilterExpression = typing.Union[FilterPredicate, "FilterAnd", "FilterOr"] diff --git a/src/supermemory/types/filter_number_compare.py b/src/supermemory/types/filter_number_compare.py new file mode 100644 index 00000000..2ebf3313 --- /dev/null +++ b/src/supermemory/types/filter_number_compare.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .filter_number_compare_operator import FilterNumberCompareOperator + + +class FilterNumberCompare(UncheckedBaseModel): + field: str = pydantic.Field() + """ + Metadata key to evaluate, matched literally. A period is part of the key name, not a path into nested metadata. + """ + + operator: FilterNumberCompareOperator = pydantic.Field() + """ + Numeric comparison to apply + """ + + value: float = pydantic.Field() + """ + Numeric value to compare against + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/filter_number_compare_operator.py b/src/supermemory/types/filter_number_compare_operator.py new file mode 100644 index 00000000..5b2fba9d --- /dev/null +++ b/src/supermemory/types/filter_number_compare_operator.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +FilterNumberCompareOperator = typing.Union[typing.Literal["gt", "gte", "lt", "lte"], typing.Any] diff --git a/src/supermemory/types/filter_or.py b/src/supermemory/types/filter_or.py new file mode 100644 index 00000000..7e4f4326 --- /dev/null +++ b/src/supermemory/types/filter_or.py @@ -0,0 +1,36 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, update_forward_refs +from ..core.unchecked_base_model import UncheckedBaseModel + + +class FilterOr(UncheckedBaseModel): + operator: typing.Literal["or"] = pydantic.Field(default="or") + """ + Require at least one nested filter expression to match + """ + + operands: typing.List["FilterExpression"] = pydantic.Field() + """ + Filter expressions combined with logical OR + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow + + +from .filter_and import FilterAnd # noqa: E402, I001 +from .filter_expression import FilterExpression # noqa: E402, I001 + +update_forward_refs(FilterOr, FilterAnd=FilterAnd, FilterExpression=FilterExpression) diff --git a/src/supermemory/types/filter_predicate.py b/src/supermemory/types/filter_predicate.py new file mode 100644 index 00000000..4e4c3ab9 --- /dev/null +++ b/src/supermemory/types/filter_predicate.py @@ -0,0 +1,13 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .filter_array_contains import FilterArrayContains +from .filter_number_compare import FilterNumberCompare +from .filter_string_contains import FilterStringContains +from .filter_string_equals import FilterStringEquals +from .filter_value_equals import FilterValueEquals + +FilterPredicate = typing.Union[ + FilterStringEquals, FilterValueEquals, FilterNumberCompare, FilterStringContains, FilterArrayContains +] diff --git a/src/supermemory/types/filter_string_contains.py b/src/supermemory/types/filter_string_contains.py new file mode 100644 index 00000000..8ed6fa5f --- /dev/null +++ b/src/supermemory/types/filter_string_contains.py @@ -0,0 +1,45 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .filter_string_contains_operator import FilterStringContainsOperator + + +class FilterStringContains(UncheckedBaseModel): + field: str = pydantic.Field() + """ + Metadata key to evaluate, matched literally. A period is part of the key name, not a path into nested metadata. + """ + + operator: FilterStringContainsOperator = pydantic.Field() + """ + Require or exclude a substring match + """ + + value: str = pydantic.Field() + """ + Substring to look for + """ + + case_sensitive: typing_extensions.Annotated[ + typing.Optional[bool], + FieldMetadata(alias="caseSensitive"), + pydantic.Field(alias="caseSensitive", description="Whether substring matching preserves letter case"), + ] = None + """ + Whether substring matching preserves letter case + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/filter_string_contains_operator.py b/src/supermemory/types/filter_string_contains_operator.py new file mode 100644 index 00000000..5712d11e --- /dev/null +++ b/src/supermemory/types/filter_string_contains_operator.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +FilterStringContainsOperator = typing.Union[typing.Literal["contains", "notContains"], typing.Any] diff --git a/src/supermemory/types/filter_string_equals.py b/src/supermemory/types/filter_string_equals.py new file mode 100644 index 00000000..b70caedb --- /dev/null +++ b/src/supermemory/types/filter_string_equals.py @@ -0,0 +1,45 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .filter_string_equals_operator import FilterStringEqualsOperator + + +class FilterStringEquals(UncheckedBaseModel): + field: str = pydantic.Field() + """ + Metadata key to evaluate, matched literally. A period is part of the key name, not a path into nested metadata. + """ + + operator: FilterStringEqualsOperator = pydantic.Field() + """ + Compare the field for equality or inequality + """ + + value: str = pydantic.Field() + """ + String value to compare against + """ + + case_sensitive: typing_extensions.Annotated[ + typing.Optional[bool], + FieldMetadata(alias="caseSensitive"), + pydantic.Field(alias="caseSensitive", description="Whether string comparison preserves letter case"), + ] = None + """ + Whether string comparison preserves letter case + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/filter_string_equals_operator.py b/src/supermemory/types/filter_string_equals_operator.py new file mode 100644 index 00000000..d729c9c8 --- /dev/null +++ b/src/supermemory/types/filter_string_equals_operator.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +FilterStringEqualsOperator = typing.Union[typing.Literal["eq", "neq"], typing.Any] diff --git a/src/supermemory/types/filter_value_equals.py b/src/supermemory/types/filter_value_equals.py new file mode 100644 index 00000000..12726ec7 --- /dev/null +++ b/src/supermemory/types/filter_value_equals.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .filter_value_equals_operator import FilterValueEqualsOperator +from .filter_value_equals_value import FilterValueEqualsValue + + +class FilterValueEquals(UncheckedBaseModel): + field: str = pydantic.Field() + """ + Metadata key to evaluate, matched literally. A period is part of the key name, not a path into nested metadata. + """ + + operator: FilterValueEqualsOperator = pydantic.Field() + """ + Compare the field for equality or inequality + """ + + value: FilterValueEqualsValue = pydantic.Field() + """ + Numeric or boolean value to compare against + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/filter_value_equals_operator.py b/src/supermemory/types/filter_value_equals_operator.py new file mode 100644 index 00000000..7d030d29 --- /dev/null +++ b/src/supermemory/types/filter_value_equals_operator.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +FilterValueEqualsOperator = typing.Union[typing.Literal["eq", "neq"], typing.Any] diff --git a/src/supermemory/types/filter_value_equals_value.py b/src/supermemory/types/filter_value_equals_value.py new file mode 100644 index 00000000..f8684378 --- /dev/null +++ b/src/supermemory/types/filter_value_equals_value.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +FilterValueEqualsValue = typing.Union[float, bool] diff --git a/src/supermemory/types/forget_error.py b/src/supermemory/types/forget_error.py new file mode 100644 index 00000000..1f1043b6 --- /dev/null +++ b/src/supermemory/types/forget_error.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ForgetError(UncheckedBaseModel): + id: str = pydantic.Field() + """ + Requested memory identifier + """ + + error: str = pydantic.Field() + """ + Why the memory could not be forgotten + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/forget_match.py b/src/supermemory/types/forget_match.py new file mode 100644 index 00000000..edd4ae3d --- /dev/null +++ b/src/supermemory/types/forget_match.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ForgetMatch(UncheckedBaseModel): + id: str = pydantic.Field() + """ + Stable identifier of the matched memory + """ + + memory: str = pydantic.Field() + """ + Matched memory text + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/forget_result.py b/src/supermemory/types/forget_result.py new file mode 100644 index 00000000..b6489b8e --- /dev/null +++ b/src/supermemory/types/forget_result.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .forget_error import ForgetError +from .forget_match import ForgetMatch + + +class ForgetResult(UncheckedBaseModel): + count: int = pydantic.Field() + """ + Number of memories matched by this operation + """ + + errors: typing.List[ForgetError] = pydantic.Field() + """ + Per-memory failures for exact-ID requests + """ + + matches: typing.List[ForgetMatch] = pydantic.Field() + """ + Memories selected by the operation, whether previewed or forgotten + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/google_drive_config.py b/src/supermemory/types/google_drive_config.py new file mode 100644 index 00000000..0c645a66 --- /dev/null +++ b/src/supermemory/types/google_drive_config.py @@ -0,0 +1,33 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .google_drive_config_sync_scope import GoogleDriveConfigSyncScope + + +class GoogleDriveConfig(UncheckedBaseModel): + sync_scope: typing_extensions.Annotated[ + typing.Optional[GoogleDriveConfigSyncScope], + FieldMetadata(alias="syncScope"), + pydantic.Field( + alias="syncScope", + description="selected (default) syncs only files the user picks. full syncs the whole Drive.", + ), + ] = None + """ + selected (default) syncs only files the user picks. full syncs the whole Drive. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/google_drive_config_sync_scope.py b/src/supermemory/types/google_drive_config_sync_scope.py new file mode 100644 index 00000000..79f7fd60 --- /dev/null +++ b/src/supermemory/types/google_drive_config_sync_scope.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +GoogleDriveConfigSyncScope = typing.Union[typing.Literal["selected", "full"], typing.Any] diff --git a/src/supermemory/types/granola_config.py b/src/supermemory/types/granola_config.py new file mode 100644 index 00000000..2f5e38ba --- /dev/null +++ b/src/supermemory/types/granola_config.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class GranolaConfig(UncheckedBaseModel): + """ + Granola API key. Stored encrypted and never returned. + """ + + api_key: typing_extensions.Annotated[str, FieldMetadata(alias="apiKey"), pydantic.Field(alias="apiKey")] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/list_request_limit.py b/src/supermemory/types/list_request_limit.py new file mode 100644 index 00000000..5b4838d9 --- /dev/null +++ b/src/supermemory/types/list_request_limit.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +ListRequestLimit = typing.Union[str, float] diff --git a/src/supermemory/types/list_request_page.py b/src/supermemory/types/list_request_page.py new file mode 100644 index 00000000..d06e301e --- /dev/null +++ b/src/supermemory/types/list_request_page.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +ListRequestPage = typing.Union[str, float] diff --git a/src/supermemory/types/list_response.py b/src/supermemory/types/list_response.py new file mode 100644 index 00000000..6062cefb --- /dev/null +++ b/src/supermemory/types/list_response.py @@ -0,0 +1,46 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .chunk_list_item import ChunkListItem +from .document_list_item import DocumentListItem +from .memory import Memory +from .pagination import Pagination + + +class ListResponse(UncheckedBaseModel): + """ + A namespace-scoped page of one resource type + """ + + documents: typing.List[DocumentListItem] = pydantic.Field() + """ + Documents on this page; empty when a different resource type is requested + """ + + chunks: typing.List[ChunkListItem] = pydantic.Field() + """ + Chunks on this page; empty when a different resource type is requested + """ + + memories: typing.List[Memory] = pydantic.Field() + """ + Memories on this page; empty when a different resource type is requested + """ + + pagination: Pagination = pydantic.Field() + """ + Page metadata for the selected resource collection + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/list_sort.py b/src/supermemory/types/list_sort.py new file mode 100644 index 00000000..dccadffd --- /dev/null +++ b/src/supermemory/types/list_sort.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +ListSort = typing.Union[typing.Literal["createdAt", "updatedAt", "position"], typing.Any] diff --git a/src/supermemory/types/list_type.py b/src/supermemory/types/list_type.py new file mode 100644 index 00000000..243a16a7 --- /dev/null +++ b/src/supermemory/types/list_type.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +ListType = typing.Union[typing.Literal["documents", "chunks", "memories"], typing.Any] diff --git a/src/supermemory/types/memory.py b/src/supermemory/types/memory.py new file mode 100644 index 00000000..acb82844 --- /dev/null +++ b/src/supermemory/types/memory.py @@ -0,0 +1,88 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .memory_system import MemorySystem + + +class Memory(UncheckedBaseModel): + """ + A memory formed from documents in this namespace + """ + + id: str = pydantic.Field() + """ + Stable memory identifier + """ + + memory: str = pydantic.Field() + """ + Learned fact or context extracted from the document + """ + + metadata: typing.Optional[typing.Dict[str, typing.Any]] = pydantic.Field(default=None) + """ + Memory metadata, including temporal context when available + """ + + is_static: typing_extensions.Annotated[ + bool, + FieldMetadata(alias="isStatic"), + pydantic.Field(alias="isStatic", description="Whether the memory belongs to the stable profile section"), + ] + """ + Whether the memory belongs to the stable profile section + """ + + is_inference: typing_extensions.Annotated[ + bool, + FieldMetadata(alias="isInference"), + pydantic.Field( + alias="isInference", description="Whether the memory was inferred rather than explicitly stated" + ), + ] + """ + Whether the memory was inferred rather than explicitly stated + """ + + is_latest: typing_extensions.Annotated[ + bool, + FieldMetadata(alias="isLatest"), + pydantic.Field(alias="isLatest", description="Whether this is the newest version in its memory chain"), + ] + """ + Whether this is the newest version in its memory chain + """ + + is_forgotten: typing_extensions.Annotated[ + bool, + FieldMetadata(alias="isForgotten"), + pydantic.Field(alias="isForgotten", description="Whether this memory has been excluded from normal recall"), + ] + """ + Whether this memory has been excluded from normal recall + """ + + version: int = pydantic.Field() + """ + Version number within the memory chain + """ + + system: MemorySystem = pydantic.Field() + """ + Lifecycle timestamps maintained by Supermemory + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/memory_forget_params.py b/src/supermemory/types/memory_forget_params.py deleted file mode 100644 index bea67e5f..00000000 --- a/src/supermemory/types/memory_forget_params.py +++ /dev/null @@ -1,26 +0,0 @@ -# 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__ = ["MemoryForgetParams"] - - -class MemoryForgetParams(TypedDict, total=False): - container_tag: Required[Annotated[str, PropertyInfo(alias="containerTag")]] - """Container tag / space identifier. Required to scope the operation.""" - - id: str - """ID of the memory entry to operate on""" - - content: str - """Exact content match of the memory entry to operate on. - - Use this when you don't have the ID. - """ - - reason: str - """Optional reason for forgetting this memory""" diff --git a/src/supermemory/types/memory_forget_response.py b/src/supermemory/types/memory_forget_response.py deleted file mode 100644 index 0e000619..00000000 --- a/src/supermemory/types/memory_forget_response.py +++ /dev/null @@ -1,15 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from .._models import BaseModel - -__all__ = ["MemoryForgetResponse"] - - -class MemoryForgetResponse(BaseModel): - """Response after forgetting a memory""" - - id: str - """ID of the memory that was forgotten""" - - forgotten: bool - """Indicates the memory was successfully forgotten""" diff --git a/src/supermemory/types/memory_relation.py b/src/supermemory/types/memory_relation.py new file mode 100644 index 00000000..51cc2de2 --- /dev/null +++ b/src/supermemory/types/memory_relation.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +MemoryRelation = typing.Union[typing.Literal["updates", "extends", "derives"], typing.Any] diff --git a/src/supermemory/types/memory_system.py b/src/supermemory/types/memory_system.py new file mode 100644 index 00000000..0bba13af --- /dev/null +++ b/src/supermemory/types/memory_system.py @@ -0,0 +1,43 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class MemorySystem(UncheckedBaseModel): + """ + Lifecycle timestamps maintained by Supermemory + """ + + created_at: typing_extensions.Annotated[ + dt.datetime, + FieldMetadata(alias="createdAt"), + pydantic.Field(alias="createdAt", description="ISO 8601 timestamp when this memory version was created"), + ] + """ + ISO 8601 timestamp when this memory version was created + """ + + updated_at: typing_extensions.Annotated[ + dt.datetime, + FieldMetadata(alias="updatedAt"), + pydantic.Field(alias="updatedAt", description="ISO 8601 timestamp of the latest update to this version"), + ] + """ + ISO 8601 timestamp of the latest update to this version + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/memory_update_memory_params.py b/src/supermemory/types/memory_update_memory_params.py deleted file mode 100644 index 1d135888..00000000 --- a/src/supermemory/types/memory_update_memory_params.py +++ /dev/null @@ -1,64 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Dict, Union, Optional -from typing_extensions import Required, Annotated, TypedDict - -from .._types import SequenceNotStr -from .._utils import PropertyInfo - -__all__ = ["MemoryUpdateMemoryParams", "TemporalContext"] - - -class MemoryUpdateMemoryParams(TypedDict, total=False): - container_tag: Required[Annotated[str, PropertyInfo(alias="containerTag")]] - """Container tag / space identifier. Required to scope the operation.""" - - new_content: Required[Annotated[str, PropertyInfo(alias="newContent")]] - """The new content that will replace the existing memory""" - - id: str - """ID of the memory entry to operate on""" - - content: str - """Exact content match of the memory entry to operate on. - - Use this when you don't have the ID. - """ - - forget_after: Annotated[Optional[str], PropertyInfo(alias="forgetAfter")] - """ISO 8601 datetime string. - - The memory will be auto-forgotten after this time. Pass null to clear an - existing expiry. Omit to inherit from the previous version. - """ - - forget_reason: Annotated[Optional[str], PropertyInfo(alias="forgetReason")] - """Optional reason for the scheduled forgetting. - - Cleared automatically when forgetAfter is set to null. - """ - - metadata: Dict[str, Union[str, float, bool, SequenceNotStr[str]]] - """Optional metadata. If not provided, inherits from the previous version.""" - - temporal_context: Annotated[TemporalContext, PropertyInfo(alias="temporalContext")] - """Structured temporal metadata. - - Merged into the metadata JSON column. If omitted, existing temporalContext is - preserved. - """ - - -class TemporalContext(TypedDict, total=False): - """Structured temporal metadata. - - Merged into the metadata JSON column. If omitted, existing temporalContext is preserved. - """ - - document_date: Annotated[Optional[str], PropertyInfo(alias="documentDate")] - """Date the document was authored""" - - event_date: Annotated[Optional[SequenceNotStr[str]], PropertyInfo(alias="eventDate")] - """Dates of events referenced in the memory""" diff --git a/src/supermemory/types/memory_update_memory_response.py b/src/supermemory/types/memory_update_memory_response.py deleted file mode 100644 index e3ff11ea..00000000 --- a/src/supermemory/types/memory_update_memory_response.py +++ /dev/null @@ -1,43 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict, Optional - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["MemoryUpdateMemoryResponse"] - - -class MemoryUpdateMemoryResponse(BaseModel): - """Response after updating a memory""" - - id: str - """ID of the newly created memory version""" - - created_at: str = FieldInfo(alias="createdAt") - """When this memory version was created""" - - forget_after: Optional[str] = FieldInfo(alias="forgetAfter", default=None) - """When this memory will be auto-forgotten, or null if no expiry""" - - forget_reason: Optional[str] = FieldInfo(alias="forgetReason", default=None) - """Reason for the scheduled forgetting, or null""" - - memory: str - """The content of the new memory version""" - - metadata: Optional[Dict[str, object]] = None - """ - Arbitrary key-value metadata attached to this memory, or null if none was - provided - """ - - parent_memory_id: Optional[str] = FieldInfo(alias="parentMemoryId", default=None) - """ID of the memory this version updates""" - - root_memory_id: Optional[str] = FieldInfo(alias="rootMemoryId", default=None) - """ID of the first memory in this version chain""" - - version: float - """Version number of this memory entry""" diff --git a/src/supermemory/types/metadata_value.py b/src/supermemory/types/metadata_value.py new file mode 100644 index 00000000..029fbbb2 --- /dev/null +++ b/src/supermemory/types/metadata_value.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +MetadataValue = typing.Union[str, float, bool, typing.List[str]] diff --git a/src/supermemory/types/namespace.py b/src/supermemory/types/namespace.py new file mode 100644 index 00000000..d97b5ca1 --- /dev/null +++ b/src/supermemory/types/namespace.py @@ -0,0 +1,59 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .namespace_system import NamespaceSystem + + +class Namespace(UncheckedBaseModel): + id: str = pydantic.Field() + """ + Internal identifier for the namespace + """ + + namespace: str = pydantic.Field() + """ + Namespace identifier used in API paths + """ + + document_count: typing_extensions.Annotated[ + int, + FieldMetadata(alias="documentCount"), + pydantic.Field(alias="documentCount", description="Number of active source documents in the namespace"), + ] + """ + Number of active source documents in the namespace + """ + + memory_count: typing_extensions.Annotated[ + int, + FieldMetadata(alias="memoryCount"), + pydantic.Field(alias="memoryCount", description="Number of current, recallable memories in the namespace"), + ] + """ + Number of current, recallable memories in the namespace + """ + + description: typing.Optional[str] = pydantic.Field(default=None) + """ + Human-readable purpose or scope of the namespace + """ + + system: NamespaceSystem = pydantic.Field() + """ + Namespace lifecycle timestamps + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/namespace_deleted.py b/src/supermemory/types/namespace_deleted.py new file mode 100644 index 00000000..6ca9fbb1 --- /dev/null +++ b/src/supermemory/types/namespace_deleted.py @@ -0,0 +1,48 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class NamespaceDeleted(UncheckedBaseModel): + success: typing.Literal[True] = pydantic.Field(default=True) + """ + Confirms the namespace was deleted + """ + + namespace: str = pydantic.Field() + """ + Deleted namespace identifier + """ + + deleted_documents_count: typing_extensions.Annotated[ + int, + FieldMetadata(alias="deletedDocumentsCount"), + pydantic.Field(alias="deletedDocumentsCount", description="Number of documents permanently removed"), + ] + """ + Number of documents permanently removed + """ + + deleted_memories_count: typing_extensions.Annotated[ + int, + FieldMetadata(alias="deletedMemoriesCount"), + pydantic.Field(alias="deletedMemoriesCount", description="Number of memories permanently removed"), + ] + """ + Number of memories permanently removed + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/namespace_details.py b/src/supermemory/types/namespace_details.py new file mode 100644 index 00000000..070eace5 --- /dev/null +++ b/src/supermemory/types/namespace_details.py @@ -0,0 +1,43 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .namespace_system import NamespaceSystem + + +class NamespaceDetails(UncheckedBaseModel): + namespace: str = pydantic.Field() + """ + Namespace identifier used in API paths + """ + + supporting_context: typing_extensions.Annotated[ + typing.Optional[str], + FieldMetadata(alias="supportingContext"), + pydantic.Field( + alias="supportingContext", + description="Background that helps Supermemory interpret documents and form better memories in this namespace", + ), + ] = None + """ + Background that helps Supermemory interpret documents and form better memories in this namespace + """ + + system: NamespaceSystem = pydantic.Field() + """ + Namespace lifecycle timestamps + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/namespace_system.py b/src/supermemory/types/namespace_system.py new file mode 100644 index 00000000..6c1cf7a8 --- /dev/null +++ b/src/supermemory/types/namespace_system.py @@ -0,0 +1,43 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class NamespaceSystem(UncheckedBaseModel): + """ + Namespace lifecycle timestamps + """ + + created_at: typing_extensions.Annotated[ + dt.datetime, + FieldMetadata(alias="createdAt"), + pydantic.Field(alias="createdAt", description="ISO 8601 timestamp when the namespace was created"), + ] + """ + ISO 8601 timestamp when the namespace was created + """ + + updated_at: typing_extensions.Annotated[ + dt.datetime, + FieldMetadata(alias="updatedAt"), + pydantic.Field(alias="updatedAt", description="ISO 8601 timestamp of the latest namespace update"), + ] + """ + ISO 8601 timestamp of the latest namespace update + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/organization.py b/src/supermemory/types/organization.py new file mode 100644 index 00000000..defae8a1 --- /dev/null +++ b/src/supermemory/types/organization.py @@ -0,0 +1,41 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class Organization(UncheckedBaseModel): + organizational_context: typing_extensions.Annotated[ + typing.Optional[str], + FieldMetadata(alias="organizationalContext"), + pydantic.Field( + alias="organizationalContext", + description="Shared background that helps Supermemory interpret content consistently across every namespace", + ), + ] = None + """ + Shared background that helps Supermemory interpret content consistently across every namespace + """ + + namespace_count: typing_extensions.Annotated[ + int, + FieldMetadata(alias="namespaceCount"), + pydantic.Field(alias="namespaceCount", description="Number of namespaces in the organization"), + ] + """ + Number of namespaces in the organization + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/pagination.py b/src/supermemory/types/pagination.py new file mode 100644 index 00000000..974992f5 --- /dev/null +++ b/src/supermemory/types/pagination.py @@ -0,0 +1,56 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class Pagination(UncheckedBaseModel): + """ + Page metadata for the selected resource collection + """ + + current_page: typing_extensions.Annotated[ + float, + FieldMetadata(alias="currentPage"), + pydantic.Field(alias="currentPage", description="Current one-based page number"), + ] + """ + Current one-based page number + """ + + limit: typing.Optional[float] = pydantic.Field(default=None) + """ + Maximum resources returned per page + """ + + total_items: typing_extensions.Annotated[ + float, + FieldMetadata(alias="totalItems"), + pydantic.Field(alias="totalItems", description="Total resources matching the request"), + ] + """ + Total resources matching the request + """ + + total_pages: typing_extensions.Annotated[ + float, + FieldMetadata(alias="totalPages"), + pydantic.Field(alias="totalPages", description="Total pages available at the selected limit"), + ] + """ + Total pages available at the selected limit + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/profile.py b/src/supermemory/types/profile.py new file mode 100644 index 00000000..b8b52318 --- /dev/null +++ b/src/supermemory/types/profile.py @@ -0,0 +1,38 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .profile_fact import ProfileFact + + +class Profile(UncheckedBaseModel): + """ + Continuously maintained understanding of the subject represented by this namespace + """ + + static: typing.List[ProfileFact] = pydantic.Field() + """ + Durable facts that define the subject and rarely change, such as identity or long-term preferences + """ + + dynamic: typing.List[ProfileFact] = pydantic.Field() + """ + Recent or evolving context, such as current goals, projects, and active interests + """ + + buckets: typing.Dict[str, typing.List[ProfileFact]] = pydantic.Field() + """ + Custom profile sections keyed by bucket name, with the memories currently assigned to each bucket + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/profile_buckets.py b/src/supermemory/types/profile_buckets.py new file mode 100644 index 00000000..b7910b44 --- /dev/null +++ b/src/supermemory/types/profile_buckets.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ProfileBuckets(UncheckedBaseModel): + buckets: typing.Dict[str, str] = pydantic.Field() + """ + Effective bucket definitions keyed by name. Includes organization buckets and namespace-owned additions. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/profile_fact.py b/src/supermemory/types/profile_fact.py new file mode 100644 index 00000000..2f0e6549 --- /dev/null +++ b/src/supermemory/types/profile_fact.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ProfileFact(UncheckedBaseModel): + id: str = pydantic.Field() + """ + Memory ID, or an aggregated_-prefixed ID for a summary synthesized from several memories + """ + + memory: str = pydantic.Field() + """ + Memory text + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/profile_response.py b/src/supermemory/types/profile_response.py index 2510e099..38e2d456 100644 --- a/src/supermemory/types/profile_response.py +++ b/src/supermemory/types/profile_response.py @@ -1,200 +1,24 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +# This file was auto-generated by Fern from our API Definition. -from typing import Dict, List, Optional -from typing_extensions import Literal +import typing -from pydantic import Field as FieldInfo +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .profile import Profile -from .._models import BaseModel -__all__ = [ - "ProfileResponse", - "Profile", - "SearchResults", - "SearchResultsResult", - "SearchResultsResultChunk", - "SearchResultsResultContext", - "SearchResultsResultContextChild", - "SearchResultsResultContextParent", - "SearchResultsResultContextRelated", - "SearchResultsResultDocument", -] - - -class Profile(BaseModel): - buckets: Optional[Dict[str, List[str]]] = None - """Per-bucket memory lists, keyed by bucket key""" - - dynamic: Optional[List[str]] = None - """Dynamic profile information (recent memories)""" - - static: Optional[List[str]] = None - """Static profile information that remains relevant long-term""" - - -class SearchResultsResultChunk(BaseModel): - content: str - """Content of the chunk""" - - document_id: str = FieldInfo(alias="documentId") - """ID of the document this chunk belongs to""" - - position: float - """Position of chunk in the document (0-indexed)""" - - -class SearchResultsResultContextChild(BaseModel): - memory: str - """The contextual memory content""" - - relation: Literal["updates", "extends", "derives"] - """Relation type between this memory and its parent/child""" - - updated_at: str = FieldInfo(alias="updatedAt") - """Contextual memory last update date""" - - metadata: Optional[Dict[str, object]] = None - """Contextual memory metadata""" - - version: Optional[float] = None - """ - Relative version distance from the primary memory (+1 for direct child, +2 for - grand-child, etc.) - """ - - -class SearchResultsResultContextParent(BaseModel): - memory: str - """The contextual memory content""" - - relation: Literal["updates", "extends", "derives"] - """Relation type between this memory and its parent/child""" - - updated_at: str = FieldInfo(alias="updatedAt") - """Contextual memory last update date""" - - metadata: Optional[Dict[str, object]] = None - """Contextual memory metadata""" - - version: Optional[float] = None - """ - Relative version distance from the primary memory (-1 for direct parent, -2 for - grand-parent, etc.) - """ - - -class SearchResultsResultContextRelated(BaseModel): - memory: str - """The related memory content""" - - relation: Literal["extends", "derives"] - """Relation type""" - - updated_at: str = FieldInfo(alias="updatedAt") - """Related memory last update date""" - - metadata: Optional[Dict[str, object]] = None - """Related memory metadata""" - - -class SearchResultsResultContext(BaseModel): - """ - Object containing version history (parents/children via updates) and related memories (extends/derives) - """ - - children: Optional[List[SearchResultsResultContextChild]] = None - - parents: Optional[List[SearchResultsResultContextParent]] = None - - related: Optional[List[SearchResultsResultContextRelated]] = None - - -class SearchResultsResultDocument(BaseModel): - id: str - """Document ID""" - - created_at: str = FieldInfo(alias="createdAt") - """Document creation date""" - - updated_at: str = FieldInfo(alias="updatedAt") - """Document last update date""" - - metadata: Optional[Dict[str, object]] = None - """Document metadata (only included when documents=true)""" - - summary: Optional[str] = None - """Document summary (only included when summaries=true)""" - - title: Optional[str] = None - """Document title (only included when documents=true)""" - - type: Optional[str] = None - """Document type (only included when documents=true)""" - - -class SearchResultsResult(BaseModel): - id: str - """Memory entry ID or chunk ID""" - - metadata: Optional[Dict[str, object]] = None - """Memory metadata""" - - similarity: float - """Similarity score between the query and memory entry""" - - updated_at: str = FieldInfo(alias="updatedAt") - """Memory last update date""" - - chunk: Optional[str] = None - """The chunk content (only present for chunk results from hybrid search)""" - - chunks: Optional[List[SearchResultsResultChunk]] = None - """Relevant chunks from associated documents (only included when chunks=true)""" - - context: Optional[SearchResultsResultContext] = None +class ProfileResponse(UncheckedBaseModel): + profile: Profile = pydantic.Field() """ - Object containing version history (parents/children via updates) and related - memories (extends/derives) - """ - - documents: Optional[List[SearchResultsResultDocument]] = None - """Associated documents for this memory entry""" - - filepath: Optional[str] = None - """Filepath of the source document this memory or chunk came from""" - - is_aggregated: Optional[bool] = FieldInfo(alias="isAggregated", default=None) - """Indicates if this memory was created by aggregating multiple source memories""" - - memory: Optional[str] = None - """The memory content (only present for memory results)""" - - root_memory_id: Optional[str] = FieldInfo(alias="rootMemoryId", default=None) - """ID of the root (first version) memory entry this one descends from. - - Null for memories that have never been superseded. Only present on memory - results, not on standalone chunk results. + Continuously maintained understanding of the subject represented by this namespace """ - version: Optional[float] = None - """Version number of this memory entry""" - - -class SearchResults(BaseModel): - """Search results if a search query was provided""" - - results: List[SearchResultsResult] - """Search results for the provided query""" - - timing: float - """Search timing in milliseconds""" - - total: float - """Total number of search results""" - - -class ProfileResponse(BaseModel): - profile: Profile + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: - search_results: Optional[SearchResults] = FieldInfo(alias="searchResults", default=None) - """Search results if a search query was provided""" + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/related_memories.py b/src/supermemory/types/related_memories.py new file mode 100644 index 00000000..7eb33ded --- /dev/null +++ b/src/supermemory/types/related_memories.py @@ -0,0 +1,38 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .related_memory import RelatedMemory + + +class RelatedMemories(UncheckedBaseModel): + """ + Memory relationships attached when requested + """ + + parents: typing.List[RelatedMemory] = pydantic.Field() + """ + Earlier memories this result updates or derives from + """ + + children: typing.List[RelatedMemory] = pydantic.Field() + """ + Newer memories that update or extend this result + """ + + siblings: typing.List[RelatedMemory] = pydantic.Field() + """ + Other memories derived from related context + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/related_memory.py b/src/supermemory/types/related_memory.py new file mode 100644 index 00000000..a1972e77 --- /dev/null +++ b/src/supermemory/types/related_memory.py @@ -0,0 +1,45 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .memory_relation import MemoryRelation +from .related_memory_system import RelatedMemorySystem + + +class RelatedMemory(UncheckedBaseModel): + relation: MemoryRelation = pydantic.Field() + """ + How this memory is connected to the matched memory + """ + + version: typing.Optional[float] = pydantic.Field(default=None) + """ + Version number within the related memory's history + """ + + memory: str = pydantic.Field() + """ + Related learned fact or context + """ + + metadata: typing.Optional[typing.Dict[str, typing.Any]] = pydantic.Field(default=None) + """ + Public metadata associated with the related memory + """ + + system: RelatedMemorySystem = pydantic.Field() + """ + Lifecycle details for the related memory + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/related_memory_system.py b/src/supermemory/types/related_memory_system.py new file mode 100644 index 00000000..ea572cb7 --- /dev/null +++ b/src/supermemory/types/related_memory_system.py @@ -0,0 +1,33 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class RelatedMemorySystem(UncheckedBaseModel): + """ + Lifecycle details for the related memory + """ + + updated_at: typing_extensions.Annotated[ + str, + FieldMetadata(alias="updatedAt"), + pydantic.Field(alias="updatedAt", description="ISO 8601 timestamp of the related memory's latest update"), + ] + """ + ISO 8601 timestamp of the related memory's latest update + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/s3config.py b/src/supermemory/types/s3config.py new file mode 100644 index 00000000..15d8faeb --- /dev/null +++ b/src/supermemory/types/s3config.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class S3Config(UncheckedBaseModel): + """ + S3 bucket to sync. Credentials are stored encrypted and never returned. + """ + + bucket: str + region: str + access_key_id: typing_extensions.Annotated[ + str, FieldMetadata(alias="accessKeyId"), pydantic.Field(alias="accessKeyId") + ] + secret_access_key: typing_extensions.Annotated[ + str, FieldMetadata(alias="secretAccessKey"), pydantic.Field(alias="secretAccessKey") + ] + endpoint: typing.Optional[str] = pydantic.Field(default=None) + """ + For S3-compatible stores such as R2 or MinIO + """ + + prefix: typing.Optional[str] = pydantic.Field(default=None) + """ + Only sync keys under this prefix + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/search_documents_params.py b/src/supermemory/types/search_documents_params.py deleted file mode 100644 index 6d760418..00000000 --- a/src/supermemory/types/search_documents_params.py +++ /dev/null @@ -1,183 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Union, Iterable -from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict - -from .._types import SequenceNotStr -from .._utils import PropertyInfo - -__all__ = [ - "SearchDocumentsParams", - "Filters", - "FiltersOr", - "FiltersOrOr", - "FiltersOrOrFilterCondition", - "FiltersOrOrOr", - "FiltersOrOrAnd", - "FiltersAnd", - "FiltersAndAnd", - "FiltersAndAndFilterCondition", - "FiltersAndAndOr", - "FiltersAndAndAnd", -] - - -class SearchDocumentsParams(TypedDict, total=False): - q: Required[str] - """Search query string""" - - categories_filter: Annotated[SequenceNotStr[str], PropertyInfo(alias="categoriesFilter")] - """DEPRECATED: Optional category filters""" - - chunk_threshold: Annotated[float, PropertyInfo(alias="chunkThreshold")] - """Threshold / sensitivity for chunk selection. - - 0 is least sensitive (returns most chunks, more results), 1 is most sensitive - (returns lesser chunks, accurate results) - """ - - container_tag: Annotated[str, PropertyInfo(alias="containerTag")] - """Optional single container tag. Use this or containerTags.""" - - container_tags: Annotated[SequenceNotStr[str], PropertyInfo(alias="containerTags")] - """Optional tags this search should be containerized by. - - This can be an ID for your user, a project ID, or any other identifier you wish - to use to filter documents. - """ - - doc_id: Annotated[str, PropertyInfo(alias="docId")] - """Optional document ID to search within. - - You can use this to find chunks in a very large document. - """ - - document_threshold: Annotated[float, PropertyInfo(alias="documentThreshold")] - """DEPRECATED: This field is no longer used in v3 search. - - The search now uses chunkThreshold only. This parameter will be ignored. - """ - - filepath: str - """Filter search results by filepath. - - Exact match for full paths, prefix match if ending with / - """ - - filters: Filters - """Optional filters to apply to the search. Can be a JSON string or Query object.""" - - include_full_docs: Annotated[bool, PropertyInfo(alias="includeFullDocs")] - """If true, include full document in the response. - - This is helpful if you want a chatbot to know the full context of the document. - """ - - include_summary: Annotated[bool, PropertyInfo(alias="includeSummary")] - """If true, include document summary in the response. - - This is helpful if you want a chatbot to know the full context of the document. - """ - - limit: int - """Maximum number of results to return""" - - only_matching_chunks: Annotated[bool, PropertyInfo(alias="onlyMatchingChunks")] - """If true, only return matching chunks without context. - - Normally, we send the previous and next chunk to provide more context for LLMs. - If you only want the matching chunk, set this to true. - """ - - rerank: bool - """If true, rerank the results based on the query. - - This is helpful if you want to ensure the most relevant results are returned. - """ - - rewrite_query: Annotated[bool, PropertyInfo(alias="rewriteQuery")] - """If true, rewrites the query to make it easier to find documents. - - This increases the latency by about 400ms - """ - - -class FiltersOrOrFilterCondition(TypedDict, total=False): - """ - A single filter condition based on metadata, numeric values, array contents, or string matching - """ - - key: Required[str] - - value: Required[str] - - filter_type: Annotated[ - Literal["metadata", "numeric", "array_contains", "string_contains"], PropertyInfo(alias="filterType") - ] - - ignore_case: Annotated[Union[bool, Literal["true", "false"]], PropertyInfo(alias="ignoreCase")] - - negate: Union[bool, Literal["true", "false"]] - - numeric_operator: Annotated[Literal[">", "<", ">=", "<=", "="], PropertyInfo(alias="numericOperator")] - - -class FiltersOrOrOr(TypedDict, total=False): - or_: Required[Annotated[Iterable[object], PropertyInfo(alias="OR")]] - """OR: Array of conditions or nested expressions""" - - -class FiltersOrOrAnd(TypedDict, total=False): - and_: Required[Annotated[Iterable[object], PropertyInfo(alias="AND")]] - """AND: Array of conditions or nested expressions""" - - -FiltersOrOr: TypeAlias = Union[FiltersOrOrFilterCondition, FiltersOrOrOr, FiltersOrOrAnd] - - -class FiltersOr(TypedDict, total=False): - or_: Required[Annotated[Iterable[FiltersOrOr], PropertyInfo(alias="OR")]] - """Array of OR filter expressions""" - - -class FiltersAndAndFilterCondition(TypedDict, total=False): - """ - A single filter condition based on metadata, numeric values, array contents, or string matching - """ - - key: Required[str] - - value: Required[str] - - filter_type: Annotated[ - Literal["metadata", "numeric", "array_contains", "string_contains"], PropertyInfo(alias="filterType") - ] - - ignore_case: Annotated[Union[bool, Literal["true", "false"]], PropertyInfo(alias="ignoreCase")] - - negate: Union[bool, Literal["true", "false"]] - - numeric_operator: Annotated[Literal[">", "<", ">=", "<=", "="], PropertyInfo(alias="numericOperator")] - - -class FiltersAndAndOr(TypedDict, total=False): - or_: Required[Annotated[Iterable[object], PropertyInfo(alias="OR")]] - """OR: Array of conditions or nested expressions""" - - -class FiltersAndAndAnd(TypedDict, total=False): - and_: Required[Annotated[Iterable[object], PropertyInfo(alias="AND")]] - """AND: Array of conditions or nested expressions""" - - -FiltersAndAnd: TypeAlias = Union[FiltersAndAndFilterCondition, FiltersAndAndOr, FiltersAndAndAnd] - - -class FiltersAnd(TypedDict, total=False): - and_: Required[Annotated[Iterable[FiltersAndAnd], PropertyInfo(alias="AND")]] - """Array of AND filter expressions""" - - -Filters: TypeAlias = Union[FiltersOr, FiltersAnd] diff --git a/src/supermemory/types/search_documents_response.py b/src/supermemory/types/search_documents_response.py deleted file mode 100644 index 8ec35c2d..00000000 --- a/src/supermemory/types/search_documents_response.py +++ /dev/null @@ -1,69 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict, List, Optional - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["SearchDocumentsResponse", "Result", "ResultChunk"] - - -class ResultChunk(BaseModel): - """Matching content chunk""" - - content: str - """Content of the matching chunk""" - - is_relevant: bool = FieldInfo(alias="isRelevant") - """Whether this chunk is relevant to the query""" - - position: float - """Position of this chunk within the source document (0-indexed). - - Use it to order chunks and to locate surrounding context chunks - (isRelevant=false) relative to the matching ones. - """ - - score: float - """Similarity score for this chunk""" - - -class Result(BaseModel): - chunks: List[ResultChunk] - """Matching content chunks from the document""" - - created_at: str = FieldInfo(alias="createdAt") - """Document creation date""" - - document_id: str = FieldInfo(alias="documentId") - """ID of the matching document""" - - metadata: Optional[Dict[str, object]] = None - """Document metadata""" - - score: float - """Relevance score of the match""" - - title: Optional[str] = None - """Document title""" - - type: Optional[str] = None - """Document type""" - - updated_at: str = FieldInfo(alias="updatedAt") - """Document last update date""" - - content: Optional[str] = None - """Full document content (only included when includeFullDocs=true)""" - - summary: Optional[str] = None - """Document summary""" - - -class SearchDocumentsResponse(BaseModel): - results: List[Result] - - timing: float - - total: float diff --git a/src/supermemory/types/search_execute_params.py b/src/supermemory/types/search_execute_params.py deleted file mode 100644 index 20c85d21..00000000 --- a/src/supermemory/types/search_execute_params.py +++ /dev/null @@ -1,183 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Union, Iterable -from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict - -from .._types import SequenceNotStr -from .._utils import PropertyInfo - -__all__ = [ - "SearchExecuteParams", - "Filters", - "FiltersOr", - "FiltersOrOr", - "FiltersOrOrFilterCondition", - "FiltersOrOrOr", - "FiltersOrOrAnd", - "FiltersAnd", - "FiltersAndAnd", - "FiltersAndAndFilterCondition", - "FiltersAndAndOr", - "FiltersAndAndAnd", -] - - -class SearchExecuteParams(TypedDict, total=False): - q: Required[str] - """Search query string""" - - categories_filter: Annotated[SequenceNotStr[str], PropertyInfo(alias="categoriesFilter")] - """DEPRECATED: Optional category filters""" - - chunk_threshold: Annotated[float, PropertyInfo(alias="chunkThreshold")] - """Threshold / sensitivity for chunk selection. - - 0 is least sensitive (returns most chunks, more results), 1 is most sensitive - (returns lesser chunks, accurate results) - """ - - container_tag: Annotated[str, PropertyInfo(alias="containerTag")] - """Optional single container tag. Use this or containerTags.""" - - container_tags: Annotated[SequenceNotStr[str], PropertyInfo(alias="containerTags")] - """Optional tags this search should be containerized by. - - This can be an ID for your user, a project ID, or any other identifier you wish - to use to filter documents. - """ - - doc_id: Annotated[str, PropertyInfo(alias="docId")] - """Optional document ID to search within. - - You can use this to find chunks in a very large document. - """ - - document_threshold: Annotated[float, PropertyInfo(alias="documentThreshold")] - """DEPRECATED: This field is no longer used in v3 search. - - The search now uses chunkThreshold only. This parameter will be ignored. - """ - - filepath: str - """Filter search results by filepath. - - Exact match for full paths, prefix match if ending with / - """ - - filters: Filters - """Optional filters to apply to the search. Can be a JSON string or Query object.""" - - include_full_docs: Annotated[bool, PropertyInfo(alias="includeFullDocs")] - """If true, include full document in the response. - - This is helpful if you want a chatbot to know the full context of the document. - """ - - include_summary: Annotated[bool, PropertyInfo(alias="includeSummary")] - """If true, include document summary in the response. - - This is helpful if you want a chatbot to know the full context of the document. - """ - - limit: int - """Maximum number of results to return""" - - only_matching_chunks: Annotated[bool, PropertyInfo(alias="onlyMatchingChunks")] - """If true, only return matching chunks without context. - - Normally, we send the previous and next chunk to provide more context for LLMs. - If you only want the matching chunk, set this to true. - """ - - rerank: bool - """If true, rerank the results based on the query. - - This is helpful if you want to ensure the most relevant results are returned. - """ - - rewrite_query: Annotated[bool, PropertyInfo(alias="rewriteQuery")] - """If true, rewrites the query to make it easier to find documents. - - This increases the latency by about 400ms - """ - - -class FiltersOrOrFilterCondition(TypedDict, total=False): - """ - A single filter condition based on metadata, numeric values, array contents, or string matching - """ - - key: Required[str] - - value: Required[str] - - filter_type: Annotated[ - Literal["metadata", "numeric", "array_contains", "string_contains"], PropertyInfo(alias="filterType") - ] - - ignore_case: Annotated[Union[bool, Literal["true", "false"]], PropertyInfo(alias="ignoreCase")] - - negate: Union[bool, Literal["true", "false"]] - - numeric_operator: Annotated[Literal[">", "<", ">=", "<=", "="], PropertyInfo(alias="numericOperator")] - - -class FiltersOrOrOr(TypedDict, total=False): - or_: Required[Annotated[Iterable[object], PropertyInfo(alias="OR")]] - """OR: Array of conditions or nested expressions""" - - -class FiltersOrOrAnd(TypedDict, total=False): - and_: Required[Annotated[Iterable[object], PropertyInfo(alias="AND")]] - """AND: Array of conditions or nested expressions""" - - -FiltersOrOr: TypeAlias = Union[FiltersOrOrFilterCondition, FiltersOrOrOr, FiltersOrOrAnd] - - -class FiltersOr(TypedDict, total=False): - or_: Required[Annotated[Iterable[FiltersOrOr], PropertyInfo(alias="OR")]] - """Array of OR filter expressions""" - - -class FiltersAndAndFilterCondition(TypedDict, total=False): - """ - A single filter condition based on metadata, numeric values, array contents, or string matching - """ - - key: Required[str] - - value: Required[str] - - filter_type: Annotated[ - Literal["metadata", "numeric", "array_contains", "string_contains"], PropertyInfo(alias="filterType") - ] - - ignore_case: Annotated[Union[bool, Literal["true", "false"]], PropertyInfo(alias="ignoreCase")] - - negate: Union[bool, Literal["true", "false"]] - - numeric_operator: Annotated[Literal[">", "<", ">=", "<=", "="], PropertyInfo(alias="numericOperator")] - - -class FiltersAndAndOr(TypedDict, total=False): - or_: Required[Annotated[Iterable[object], PropertyInfo(alias="OR")]] - """OR: Array of conditions or nested expressions""" - - -class FiltersAndAndAnd(TypedDict, total=False): - and_: Required[Annotated[Iterable[object], PropertyInfo(alias="AND")]] - """AND: Array of conditions or nested expressions""" - - -FiltersAndAnd: TypeAlias = Union[FiltersAndAndFilterCondition, FiltersAndAndOr, FiltersAndAndAnd] - - -class FiltersAnd(TypedDict, total=False): - and_: Required[Annotated[Iterable[FiltersAndAnd], PropertyInfo(alias="AND")]] - """Array of AND filter expressions""" - - -Filters: TypeAlias = Union[FiltersOr, FiltersAnd] diff --git a/src/supermemory/types/search_execute_response.py b/src/supermemory/types/search_execute_response.py deleted file mode 100644 index 889400fe..00000000 --- a/src/supermemory/types/search_execute_response.py +++ /dev/null @@ -1,69 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict, List, Optional - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["SearchExecuteResponse", "Result", "ResultChunk"] - - -class ResultChunk(BaseModel): - """Matching content chunk""" - - content: str - """Content of the matching chunk""" - - is_relevant: bool = FieldInfo(alias="isRelevant") - """Whether this chunk is relevant to the query""" - - position: float - """Position of this chunk within the source document (0-indexed). - - Use it to order chunks and to locate surrounding context chunks - (isRelevant=false) relative to the matching ones. - """ - - score: float - """Similarity score for this chunk""" - - -class Result(BaseModel): - chunks: List[ResultChunk] - """Matching content chunks from the document""" - - created_at: str = FieldInfo(alias="createdAt") - """Document creation date""" - - document_id: str = FieldInfo(alias="documentId") - """ID of the matching document""" - - metadata: Optional[Dict[str, object]] = None - """Document metadata""" - - score: float - """Relevance score of the match""" - - title: Optional[str] = None - """Document title""" - - type: Optional[str] = None - """Document type""" - - updated_at: str = FieldInfo(alias="updatedAt") - """Document last update date""" - - content: Optional[str] = None - """Full document content (only included when includeFullDocs=true)""" - - summary: Optional[str] = None - """Document summary""" - - -class SearchExecuteResponse(BaseModel): - results: List[Result] - - timing: float - - total: float diff --git a/src/supermemory/types/search_memories_params.py b/src/supermemory/types/search_memories_params.py deleted file mode 100644 index 0e1b4a47..00000000 --- a/src/supermemory/types/search_memories_params.py +++ /dev/null @@ -1,191 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Union, Iterable -from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict - -from .._types import SequenceNotStr -from .._utils import PropertyInfo - -__all__ = [ - "SearchMemoriesParams", - "Filters", - "FiltersOr", - "FiltersOrOr", - "FiltersOrOrFilterCondition", - "FiltersOrOrOr", - "FiltersOrOrAnd", - "FiltersAnd", - "FiltersAndAnd", - "FiltersAndAndFilterCondition", - "FiltersAndAndOr", - "FiltersAndAndAnd", - "Include", -] - - -class SearchMemoriesParams(TypedDict, total=False): - q: Required[str] - """Search query string""" - - aggregate: bool - """ - If true, aggregates information from multiple memories to create new synthesized - memories. The result will be a mix of aggregated and non-aggregated memories, - reranked by relevance to the query. Works in conjunction with reranking. - """ - - container_tag: Annotated[str, PropertyInfo(alias="containerTag")] - """Optional tag this search should be containerized by. - - This can be an ID for your user, a project ID, or any other identifier you wish - to use to filter memories. - """ - - container_tags: Annotated[SequenceNotStr[str], PropertyInfo(alias="containerTags")] - """Optional tags this search should be containerized by. - - Search is scoped to memories under these tags. - """ - - filepath: str - """Filter search results by filepath. - - Exact match for full paths, prefix match if ending with / - """ - - filters: Filters - """Optional filters to apply to the search. Can be a JSON string or Query object.""" - - include: Include - - limit: int - """Maximum number of results to return""" - - rerank: bool - """If true, rerank the results based on the query. - - This is helpful if you want to ensure the most relevant results are returned. - """ - - rewrite_query: Annotated[bool, PropertyInfo(alias="rewriteQuery")] - """If true, rewrites the query to make it easier to find documents. - - This increases the latency by about 400ms - """ - - search_mode: Annotated[Literal["memories", "hybrid", "documents"], PropertyInfo(alias="searchMode")] - """Search mode. - - 'memories' searches only memory entries (default). 'hybrid' searches both - memories and document chunks. 'documents' searches only document chunks. - """ - - threshold: float - """Threshold / sensitivity for memories selection. - - 0 is least sensitive (returns most memories, more results), 1 is most sensitive - (returns lesser memories, accurate results) - """ - - -class FiltersOrOrFilterCondition(TypedDict, total=False): - """ - A single filter condition based on metadata, numeric values, array contents, or string matching - """ - - key: Required[str] - - value: Required[str] - - filter_type: Annotated[ - Literal["metadata", "numeric", "array_contains", "string_contains"], PropertyInfo(alias="filterType") - ] - - ignore_case: Annotated[Union[bool, Literal["true", "false"]], PropertyInfo(alias="ignoreCase")] - - negate: Union[bool, Literal["true", "false"]] - - numeric_operator: Annotated[Literal[">", "<", ">=", "<=", "="], PropertyInfo(alias="numericOperator")] - - -class FiltersOrOrOr(TypedDict, total=False): - or_: Required[Annotated[Iterable[object], PropertyInfo(alias="OR")]] - """OR: Array of conditions or nested expressions""" - - -class FiltersOrOrAnd(TypedDict, total=False): - and_: Required[Annotated[Iterable[object], PropertyInfo(alias="AND")]] - """AND: Array of conditions or nested expressions""" - - -FiltersOrOr: TypeAlias = Union[FiltersOrOrFilterCondition, FiltersOrOrOr, FiltersOrOrAnd] - - -class FiltersOr(TypedDict, total=False): - or_: Required[Annotated[Iterable[FiltersOrOr], PropertyInfo(alias="OR")]] - """Array of OR filter expressions""" - - -class FiltersAndAndFilterCondition(TypedDict, total=False): - """ - A single filter condition based on metadata, numeric values, array contents, or string matching - """ - - key: Required[str] - - value: Required[str] - - filter_type: Annotated[ - Literal["metadata", "numeric", "array_contains", "string_contains"], PropertyInfo(alias="filterType") - ] - - ignore_case: Annotated[Union[bool, Literal["true", "false"]], PropertyInfo(alias="ignoreCase")] - - negate: Union[bool, Literal["true", "false"]] - - numeric_operator: Annotated[Literal[">", "<", ">=", "<=", "="], PropertyInfo(alias="numericOperator")] - - -class FiltersAndAndOr(TypedDict, total=False): - or_: Required[Annotated[Iterable[object], PropertyInfo(alias="OR")]] - """OR: Array of conditions or nested expressions""" - - -class FiltersAndAndAnd(TypedDict, total=False): - and_: Required[Annotated[Iterable[object], PropertyInfo(alias="AND")]] - """AND: Array of conditions or nested expressions""" - - -FiltersAndAnd: TypeAlias = Union[FiltersAndAndFilterCondition, FiltersAndAndOr, FiltersAndAndAnd] - - -class FiltersAnd(TypedDict, total=False): - and_: Required[Annotated[Iterable[FiltersAndAnd], PropertyInfo(alias="AND")]] - """Array of AND filter expressions""" - - -Filters: TypeAlias = Union[FiltersOr, FiltersAnd] - - -class Include(TypedDict, total=False): - chunks: bool - """DEPRECATED: Use searchMode='hybrid' instead. - - If true, automatically switches to hybrid mode. This field is kept for backward - compatibility only. - """ - - documents: bool - - forgotten_memories: Annotated[bool, PropertyInfo(alias="forgottenMemories")] - """If true, include forgotten memories in search results. - - Forgotten memories are memories that have been explicitly forgotten or have - passed their expiration date. - """ - - related_memories: Annotated[bool, PropertyInfo(alias="relatedMemories")] - - summaries: bool diff --git a/src/supermemory/types/search_memories_response.py b/src/supermemory/types/search_memories_response.py deleted file mode 100644 index 16cade83..00000000 --- a/src/supermemory/types/search_memories_response.py +++ /dev/null @@ -1,186 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict, List, Optional -from typing_extensions import Literal - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = [ - "SearchMemoriesResponse", - "Result", - "ResultChunk", - "ResultContext", - "ResultContextChild", - "ResultContextParent", - "ResultContextRelated", - "ResultDocument", -] - - -class ResultChunk(BaseModel): - content: str - """Content of the chunk""" - - document_id: str = FieldInfo(alias="documentId") - """ID of the document this chunk belongs to""" - - position: float - """Position of chunk in the document (0-indexed)""" - - -class ResultContextChild(BaseModel): - memory: str - """The contextual memory content""" - - relation: Literal["updates", "extends", "derives"] - """Relation type between this memory and its parent/child""" - - updated_at: str = FieldInfo(alias="updatedAt") - """Contextual memory last update date""" - - metadata: Optional[Dict[str, object]] = None - """Contextual memory metadata""" - - version: Optional[float] = None - """ - Relative version distance from the primary memory (+1 for direct child, +2 for - grand-child, etc.) - """ - - -class ResultContextParent(BaseModel): - memory: str - """The contextual memory content""" - - relation: Literal["updates", "extends", "derives"] - """Relation type between this memory and its parent/child""" - - updated_at: str = FieldInfo(alias="updatedAt") - """Contextual memory last update date""" - - metadata: Optional[Dict[str, object]] = None - """Contextual memory metadata""" - - version: Optional[float] = None - """ - Relative version distance from the primary memory (-1 for direct parent, -2 for - grand-parent, etc.) - """ - - -class ResultContextRelated(BaseModel): - memory: str - """The related memory content""" - - relation: Literal["extends", "derives"] - """Relation type""" - - updated_at: str = FieldInfo(alias="updatedAt") - """Related memory last update date""" - - metadata: Optional[Dict[str, object]] = None - """Related memory metadata""" - - -class ResultContext(BaseModel): - """ - Object containing version history (parents/children via updates) and related memories (extends/derives) - """ - - children: Optional[List[ResultContextChild]] = None - - parents: Optional[List[ResultContextParent]] = None - - related: Optional[List[ResultContextRelated]] = None - - -class ResultDocument(BaseModel): - id: str - """Document ID""" - - created_at: str = FieldInfo(alias="createdAt") - """Document creation date""" - - updated_at: str = FieldInfo(alias="updatedAt") - """Document last update date""" - - metadata: Optional[Dict[str, object]] = None - """Document metadata (only included when documents=true)""" - - summary: Optional[str] = None - """Document summary (only included when summaries=true)""" - - title: Optional[str] = None - """Document title (only included when documents=true)""" - - type: Optional[str] = None - """Document type (only included when documents=true)""" - - -class Result(BaseModel): - id: str - """Memory entry ID or chunk ID""" - - metadata: Optional[Dict[str, object]] = None - """Memory metadata""" - - similarity: float - """Similarity score between the query and memory entry""" - - updated_at: str = FieldInfo(alias="updatedAt") - """Memory last update date""" - - chunk: Optional[str] = None - """The chunk content (only present for chunk results from hybrid search)""" - - chunks: Optional[List[ResultChunk]] = None - """Relevant chunks from associated documents (only included when chunks=true)""" - - context: Optional[ResultContext] = None - """ - Object containing version history (parents/children via updates) and related - memories (extends/derives) - """ - - documents: Optional[List[ResultDocument]] = None - """Associated documents for this memory entry""" - - filepath: Optional[str] = None - """Filepath of the source document this memory or chunk came from""" - - is_aggregated: Optional[bool] = FieldInfo(alias="isAggregated", default=None) - """Indicates if this memory was created by aggregating multiple source memories""" - - memory: Optional[str] = None - """The memory content (only present for memory results)""" - - root_memory_id: Optional[str] = FieldInfo(alias="rootMemoryId", default=None) - """ID of the root (first version) memory entry this one descends from. - - Null for memories that have never been superseded. Only present on memory - results, not on standalone chunk results. - """ - - version: Optional[float] = None - """Version number of this memory entry""" - - -class SearchMemoriesResponse(BaseModel): - results: List[Result] - """Array of matching memory entries and chunks with similarity scores. - - Contains memory results when searchMode='memories', both memory and chunk - results when searchMode='hybrid', or only chunk results when - searchMode='documents'. Memory results have 'memory' field, chunk results have - 'chunk' field. BACKWARD COMPATIBILITY: When using deprecated - include.chunks=true, only memory results are returned with chunks embedded in - them (old format). - """ - - timing: float - """Search execution time in milliseconds""" - - total: float - """Total number of results returned""" diff --git a/src/supermemory/types/search_mode.py b/src/supermemory/types/search_mode.py new file mode 100644 index 00000000..db609ce7 --- /dev/null +++ b/src/supermemory/types/search_mode.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +SearchMode = typing.Union[typing.Literal["hybrid", "memories", "chunks"], typing.Any] diff --git a/src/supermemory/types/search_request_attach.py b/src/supermemory/types/search_request_attach.py new file mode 100644 index 00000000..38b1f744 --- /dev/null +++ b/src/supermemory/types/search_request_attach.py @@ -0,0 +1,37 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class SearchRequestAttach(UncheckedBaseModel): + """ + Optional context to include alongside each matching result + """ + + documents: typing.Optional[bool] = pydantic.Field(default=None) + """ + Attach the source document for each result when one is available + """ + + related: typing.Optional[bool] = pydantic.Field(default=None) + """ + Attach parent, child, and sibling memories that explain how each memory evolved + """ + + forgotten: typing.Optional[bool] = pydantic.Field(default=None) + """ + Allow forgotten memories to participate in recall + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/search_request_rerank.py b/src/supermemory/types/search_request_rerank.py new file mode 100644 index 00000000..b24b19cd --- /dev/null +++ b/src/supermemory/types/search_request_rerank.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +SearchRequestRerank = typing.Union[typing.Literal["none", "order", "aggregate"], typing.Any] diff --git a/src/supermemory/types/search_response.py b/src/supermemory/types/search_response.py new file mode 100644 index 00000000..2db256cc --- /dev/null +++ b/src/supermemory/types/search_response.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .search_result import SearchResult + + +class SearchResponse(UncheckedBaseModel): + results: typing.List[SearchResult] = pydantic.Field() + """ + Ranked memories and source chunks matching the query + """ + + search_time: typing_extensions.Annotated[ + float, + FieldMetadata(alias="searchTime"), + pydantic.Field(alias="searchTime", description="Server-side search duration in milliseconds"), + ] + """ + Server-side search duration in milliseconds + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/search_result.py b/src/supermemory/types/search_result.py new file mode 100644 index 00000000..e80aac31 --- /dev/null +++ b/src/supermemory/types/search_result.py @@ -0,0 +1,69 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .search_result_included import SearchResultIncluded +from .search_result_system import SearchResultSystem + + +class SearchResult(UncheckedBaseModel): + id: str = pydantic.Field() + """ + Stable identifier for the matching result + """ + + memory: typing.Optional[str] = pydantic.Field(default=None) + """ + Learned fact or context returned by memory search (only present for memory results) + """ + + chunk: typing.Optional[str] = pydantic.Field(default=None) + """ + Source passage returned by chunk search (only present for chunk results from hybrid search) + """ + + metadata: typing.Optional[typing.Dict[str, typing.Any]] = pydantic.Field(default=None) + """ + Public metadata attached to the result + """ + + similarity: float = pydantic.Field() + """ + Normalized relevance score used to rank the result, from 0 to 1 + """ + + is_latest: typing_extensions.Annotated[ + bool, + FieldMetadata(alias="isLatest"), + pydantic.Field( + alias="isLatest", + description="Whether the memory is its latest version; false for recalled forgotten memories", + ), + ] + """ + Whether the memory is its latest version; false for recalled forgotten memories + """ + + system: SearchResultSystem = pydantic.Field() + """ + Lifecycle and filesystem details for the result + """ + + included: typing.Optional[SearchResultIncluded] = pydantic.Field(default=None) + """ + Requested supporting context for the result + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/search_result_included.py b/src/supermemory/types/search_result_included.py new file mode 100644 index 00000000..d66268e7 --- /dev/null +++ b/src/supermemory/types/search_result_included.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .related_memories import RelatedMemories +from .search_result_included_document import SearchResultIncludedDocument + + +class SearchResultIncluded(UncheckedBaseModel): + """ + Requested supporting context for the result + """ + + related: typing.Optional[RelatedMemories] = pydantic.Field(default=None) + """ + Memory relationships attached when requested + """ + + document: typing.Optional[SearchResultIncludedDocument] = pydantic.Field(default=None) + """ + Source document attached when requested and available + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/search_result_included_document.py b/src/supermemory/types/search_result_included_document.py new file mode 100644 index 00000000..84554132 --- /dev/null +++ b/src/supermemory/types/search_result_included_document.py @@ -0,0 +1,67 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class SearchResultIncludedDocument(UncheckedBaseModel): + """ + Source document attached when requested and available + """ + + id: str = pydantic.Field() + """ + Source document identifier + """ + + title: typing.Optional[str] = pydantic.Field(default=None) + """ + Source document title + """ + + type: typing.Optional[str] = pydantic.Field(default=None) + """ + Detected source or content type + """ + + metadata: typing.Optional[typing.Dict[str, typing.Any]] = pydantic.Field(default=None) + """ + Public metadata attached to the source document + """ + + summary: typing.Optional[str] = pydantic.Field(default=None) + """ + Generated summary of the source document + """ + + created_at: typing_extensions.Annotated[ + str, + FieldMetadata(alias="createdAt"), + pydantic.Field(alias="createdAt", description="ISO 8601 timestamp when the source document was created"), + ] + """ + ISO 8601 timestamp when the source document was created + """ + + updated_at: typing_extensions.Annotated[ + str, + FieldMetadata(alias="updatedAt"), + pydantic.Field(alias="updatedAt", description="ISO 8601 timestamp of the source document's latest update"), + ] + """ + ISO 8601 timestamp of the source document's latest update + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/search_result_system.py b/src/supermemory/types/search_result_system.py new file mode 100644 index 00000000..191f25b3 --- /dev/null +++ b/src/supermemory/types/search_result_system.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class SearchResultSystem(UncheckedBaseModel): + """ + Lifecycle and filesystem details for the result + """ + + updated_at: typing_extensions.Annotated[ + str, + FieldMetadata(alias="updatedAt"), + pydantic.Field(alias="updatedAt", description="ISO 8601 timestamp of the latest result update"), + ] + """ + ISO 8601 timestamp of the latest result update + """ + + created_at: typing_extensions.Annotated[ + typing.Optional[str], + FieldMetadata(alias="createdAt"), + pydantic.Field(alias="createdAt", description="ISO 8601 timestamp when the result was first created"), + ] = None + """ + ISO 8601 timestamp when the result was first created + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/setting_get_response.py b/src/supermemory/types/setting_get_response.py deleted file mode 100644 index f558944b..00000000 --- a/src/supermemory/types/setting_get_response.py +++ /dev/null @@ -1,62 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict, List, Union, Optional - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["SettingGetResponse", "ProfileBucket"] - - -class ProfileBucket(BaseModel): - """Definition of a single profile bucket""" - - key: str - """Stable slug for the bucket, stored on each memory""" - - description: Optional[str] = None - """What belongs in this bucket — used to guide the ingestion classifier.""" - - -class SettingGetResponse(BaseModel): - chunk_size: Optional[int] = FieldInfo(alias="chunkSize", default=None) - - exclude_items: Union[str, float, bool, Dict[str, object], List[object], None] = FieldInfo( - alias="excludeItems", default=None - ) - - filter_prompt: Optional[str] = FieldInfo(alias="filterPrompt", default=None) - - github_client_id: Optional[str] = FieldInfo(alias="githubClientId", default=None) - - github_client_secret: Optional[str] = FieldInfo(alias="githubClientSecret", default=None) - - github_custom_key_enabled: Optional[bool] = FieldInfo(alias="githubCustomKeyEnabled", default=None) - - google_drive_client_id: Optional[str] = FieldInfo(alias="googleDriveClientId", default=None) - - google_drive_client_secret: Optional[str] = FieldInfo(alias="googleDriveClientSecret", default=None) - - google_drive_custom_key_enabled: Optional[bool] = FieldInfo(alias="googleDriveCustomKeyEnabled", default=None) - - include_items: Union[str, float, bool, Dict[str, object], List[object], None] = FieldInfo( - alias="includeItems", default=None - ) - - notion_client_id: Optional[str] = FieldInfo(alias="notionClientId", default=None) - - notion_client_secret: Optional[str] = FieldInfo(alias="notionClientSecret", default=None) - - notion_custom_key_enabled: Optional[bool] = FieldInfo(alias="notionCustomKeyEnabled", default=None) - - onedrive_client_id: Optional[str] = FieldInfo(alias="onedriveClientId", default=None) - - onedrive_client_secret: Optional[str] = FieldInfo(alias="onedriveClientSecret", default=None) - - onedrive_custom_key_enabled: Optional[bool] = FieldInfo(alias="onedriveCustomKeyEnabled", default=None) - - profile_buckets: Optional[List[ProfileBucket]] = FieldInfo(alias="profileBuckets", default=None) - """Profile bucket definitions""" - - should_llm_filter: Optional[bool] = FieldInfo(alias="shouldLLMFilter", default=None) diff --git a/src/supermemory/types/setting_update_params.py b/src/supermemory/types/setting_update_params.py deleted file mode 100644 index 5e9d2c9c..00000000 --- a/src/supermemory/types/setting_update_params.py +++ /dev/null @@ -1,63 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Dict, Union, Iterable, Optional -from typing_extensions import Required, Annotated, TypedDict - -from .._utils import PropertyInfo - -__all__ = ["SettingUpdateParams", "ProfileBucket"] - - -class SettingUpdateParams(TypedDict, total=False): - chunk_size: Annotated[Optional[int], PropertyInfo(alias="chunkSize")] - - exclude_items: Annotated[ - Union[str, float, bool, Dict[str, object], Iterable[object], None], PropertyInfo(alias="excludeItems") - ] - - filter_prompt: Annotated[Optional[str], PropertyInfo(alias="filterPrompt")] - - github_client_id: Annotated[Optional[str], PropertyInfo(alias="githubClientId")] - - github_client_secret: Annotated[Optional[str], PropertyInfo(alias="githubClientSecret")] - - github_custom_key_enabled: Annotated[Optional[bool], PropertyInfo(alias="githubCustomKeyEnabled")] - - google_drive_client_id: Annotated[Optional[str], PropertyInfo(alias="googleDriveClientId")] - - google_drive_client_secret: Annotated[Optional[str], PropertyInfo(alias="googleDriveClientSecret")] - - google_drive_custom_key_enabled: Annotated[Optional[bool], PropertyInfo(alias="googleDriveCustomKeyEnabled")] - - include_items: Annotated[ - Union[str, float, bool, Dict[str, object], Iterable[object], None], PropertyInfo(alias="includeItems") - ] - - notion_client_id: Annotated[Optional[str], PropertyInfo(alias="notionClientId")] - - notion_client_secret: Annotated[Optional[str], PropertyInfo(alias="notionClientSecret")] - - notion_custom_key_enabled: Annotated[Optional[bool], PropertyInfo(alias="notionCustomKeyEnabled")] - - onedrive_client_id: Annotated[Optional[str], PropertyInfo(alias="onedriveClientId")] - - onedrive_client_secret: Annotated[Optional[str], PropertyInfo(alias="onedriveClientSecret")] - - onedrive_custom_key_enabled: Annotated[Optional[bool], PropertyInfo(alias="onedriveCustomKeyEnabled")] - - profile_buckets: Annotated[Iterable[ProfileBucket], PropertyInfo(alias="profileBuckets")] - """Profile bucket definitions""" - - should_llm_filter: Annotated[Optional[bool], PropertyInfo(alias="shouldLLMFilter")] - - -class ProfileBucket(TypedDict, total=False): - """Definition of a single profile bucket""" - - key: Required[str] - """Stable slug for the bucket, stored on each memory""" - - description: str - """What belongs in this bucket — used to guide the ingestion classifier.""" diff --git a/src/supermemory/types/setting_update_response.py b/src/supermemory/types/setting_update_response.py deleted file mode 100644 index 11f80809..00000000 --- a/src/supermemory/types/setting_update_response.py +++ /dev/null @@ -1,70 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict, List, Union, Optional - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["SettingUpdateResponse", "Updated", "UpdatedProfileBucket"] - - -class UpdatedProfileBucket(BaseModel): - """Definition of a single profile bucket""" - - key: str - """Stable slug for the bucket, stored on each memory""" - - description: Optional[str] = None - """What belongs in this bucket — used to guide the ingestion classifier.""" - - -class Updated(BaseModel): - chunk_size: Optional[int] = FieldInfo(alias="chunkSize", default=None) - - exclude_items: Union[str, float, bool, Dict[str, object], List[object], None] = FieldInfo( - alias="excludeItems", default=None - ) - - filter_prompt: Optional[str] = FieldInfo(alias="filterPrompt", default=None) - - github_client_id: Optional[str] = FieldInfo(alias="githubClientId", default=None) - - github_client_secret: Optional[str] = FieldInfo(alias="githubClientSecret", default=None) - - github_custom_key_enabled: Optional[bool] = FieldInfo(alias="githubCustomKeyEnabled", default=None) - - google_drive_client_id: Optional[str] = FieldInfo(alias="googleDriveClientId", default=None) - - google_drive_client_secret: Optional[str] = FieldInfo(alias="googleDriveClientSecret", default=None) - - google_drive_custom_key_enabled: Optional[bool] = FieldInfo(alias="googleDriveCustomKeyEnabled", default=None) - - include_items: Union[str, float, bool, Dict[str, object], List[object], None] = FieldInfo( - alias="includeItems", default=None - ) - - notion_client_id: Optional[str] = FieldInfo(alias="notionClientId", default=None) - - notion_client_secret: Optional[str] = FieldInfo(alias="notionClientSecret", default=None) - - notion_custom_key_enabled: Optional[bool] = FieldInfo(alias="notionCustomKeyEnabled", default=None) - - onedrive_client_id: Optional[str] = FieldInfo(alias="onedriveClientId", default=None) - - onedrive_client_secret: Optional[str] = FieldInfo(alias="onedriveClientSecret", default=None) - - onedrive_custom_key_enabled: Optional[bool] = FieldInfo(alias="onedriveCustomKeyEnabled", default=None) - - profile_buckets: Optional[List[UpdatedProfileBucket]] = FieldInfo(alias="profileBuckets", default=None) - """Profile bucket definitions""" - - should_llm_filter: Optional[bool] = FieldInfo(alias="shouldLLMFilter", default=None) - - -class SettingUpdateResponse(BaseModel): - org_id: str = FieldInfo(alias="orgId") - - org_slug: str = FieldInfo(alias="orgSlug") - - updated: Updated diff --git a/src/supermemory/types/sort_order.py b/src/supermemory/types/sort_order.py new file mode 100644 index 00000000..6dc54931 --- /dev/null +++ b/src/supermemory/types/sort_order.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +SortOrder = typing.Union[typing.Literal["asc", "desc"], typing.Any] diff --git a/src/supermemory/types/task_type.py b/src/supermemory/types/task_type.py new file mode 100644 index 00000000..4efaaccf --- /dev/null +++ b/src/supermemory/types/task_type.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +TaskType = typing.Union[typing.Literal["memory", "superrag"], typing.Any] diff --git a/src/supermemory/types/validation_error_response.py b/src/supermemory/types/validation_error_response.py new file mode 100644 index 00000000..8bedf727 --- /dev/null +++ b/src/supermemory/types/validation_error_response.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .validation_error_response_error_item import ValidationErrorResponseErrorItem + + +class ValidationErrorResponse(UncheckedBaseModel): + """ + Body returned when request input fails schema validation + """ + + success: typing.Literal[False] = False + error: typing.List[ValidationErrorResponseErrorItem] = pydantic.Field() + """ + Validation issues + """ + + data: typing.Optional[typing.Any] = pydantic.Field(default=None) + """ + The input that failed validation + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/validation_error_response_error_item.py b/src/supermemory/types/validation_error_response_error_item.py new file mode 100644 index 00000000..96e357ee --- /dev/null +++ b/src/supermemory/types/validation_error_response_error_item.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .validation_error_response_error_item_path_item import ValidationErrorResponseErrorItemPathItem + + +class ValidationErrorResponseErrorItem(UncheckedBaseModel): + message: str + path: typing.Optional[typing.List[ValidationErrorResponseErrorItemPathItem]] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory/types/validation_error_response_error_item_path_item.py b/src/supermemory/types/validation_error_response_error_item_path_item.py new file mode 100644 index 00000000..accc3911 --- /dev/null +++ b/src/supermemory/types/validation_error_response_error_item_path_item.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +ValidationErrorResponseErrorItemPathItem = typing.Union[str, float] diff --git a/src/supermemory/types/web_crawler_config.py b/src/supermemory/types/web_crawler_config.py new file mode 100644 index 00000000..8a7226af --- /dev/null +++ b/src/supermemory/types/web_crawler_config.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class WebCrawlerConfig(UncheckedBaseModel): + """ + Website to crawl + """ + + start_url: typing_extensions.Annotated[str, FieldMetadata(alias="startUrl"), pydantic.Field(alias="startUrl")] + crawl_depth: typing_extensions.Annotated[ + typing.Optional[int], + FieldMetadata(alias="crawlDepth"), + pydantic.Field(alias="crawlDepth", description="How many links deep to follow. Defaults to 3."), + ] = None + """ + How many links deep to follow. Defaults to 3. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/supermemory_new/lib/.keep b/src/supermemory_new/lib/.keep deleted file mode 100644 index 5e2c99fd..00000000 --- a/src/supermemory_new/lib/.keep +++ /dev/null @@ -1,4 +0,0 @@ -File generated from our OpenAPI spec by Stainless. - -This directory can be used to store custom files to expand the SDK. -It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index fd8019a9..00000000 --- a/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/__init__.py b/tests/api_resources/__init__.py deleted file mode 100644 index fd8019a9..00000000 --- a/tests/api_resources/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/test_client.py b/tests/api_resources/test_client.py deleted file mode 100644 index 171a2157..00000000 --- a/tests/api_resources/test_client.py +++ /dev/null @@ -1,244 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from supermemory import Supermemory, AsyncSupermemory -from tests.utils import assert_matches_type -from supermemory.types import AddResponse, ProfileResponse - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestClient: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_add(self, client: Supermemory) -> None: - client_ = client.add( - content="content", - ) - assert_matches_type(AddResponse, client_, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_add_with_all_params(self, client: Supermemory) -> None: - client_ = client.add( - content="content", - container_tag="containerTag", - container_tags=["string"], - custom_id="customId", - document_date="documentDate", - dreaming="instant", - entity_context="entityContext", - filepath="filepath", - filter_by_metadata={"foo": "string"}, - metadata={"foo": "string"}, - task_type="memory", - ) - assert_matches_type(AddResponse, client_, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_add(self, client: Supermemory) -> None: - response = client.with_raw_response.add( - content="content", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - client_ = response.parse() - assert_matches_type(AddResponse, client_, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_add(self, client: Supermemory) -> None: - with client.with_streaming_response.add( - content="content", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - client_ = response.parse() - assert_matches_type(AddResponse, client_, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_profile(self, client: Supermemory) -> None: - client_ = client.profile( - container_tag="containerTag", - ) - assert_matches_type(ProfileResponse, client_, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_profile_with_all_params(self, client: Supermemory) -> None: - client_ = client.profile( - container_tag="containerTag", - buckets=["string"], - filters={ - "or_": [ - { - "key": "key", - "value": "value", - "filter_type": "metadata", - "ignore_case": "true", - "negate": "true", - "numeric_operator": ">", - } - ] - }, - include=["static"], - q="q", - threshold=0, - ) - assert_matches_type(ProfileResponse, client_, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_profile(self, client: Supermemory) -> None: - response = client.with_raw_response.profile( - container_tag="containerTag", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - client_ = response.parse() - assert_matches_type(ProfileResponse, client_, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_profile(self, client: Supermemory) -> None: - with client.with_streaming_response.profile( - container_tag="containerTag", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - client_ = response.parse() - assert_matches_type(ProfileResponse, client_, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncClient: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_add(self, async_client: AsyncSupermemory) -> None: - client = await async_client.add( - content="content", - ) - assert_matches_type(AddResponse, client, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_add_with_all_params(self, async_client: AsyncSupermemory) -> None: - client = await async_client.add( - content="content", - container_tag="containerTag", - container_tags=["string"], - custom_id="customId", - document_date="documentDate", - dreaming="instant", - entity_context="entityContext", - filepath="filepath", - filter_by_metadata={"foo": "string"}, - metadata={"foo": "string"}, - task_type="memory", - ) - assert_matches_type(AddResponse, client, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_add(self, async_client: AsyncSupermemory) -> None: - response = await async_client.with_raw_response.add( - content="content", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - client = await response.parse() - assert_matches_type(AddResponse, client, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_add(self, async_client: AsyncSupermemory) -> None: - async with async_client.with_streaming_response.add( - content="content", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - client = await response.parse() - assert_matches_type(AddResponse, client, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_profile(self, async_client: AsyncSupermemory) -> None: - client = await async_client.profile( - container_tag="containerTag", - ) - assert_matches_type(ProfileResponse, client, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_profile_with_all_params(self, async_client: AsyncSupermemory) -> None: - client = await async_client.profile( - container_tag="containerTag", - buckets=["string"], - filters={ - "or_": [ - { - "key": "key", - "value": "value", - "filter_type": "metadata", - "ignore_case": "true", - "negate": "true", - "numeric_operator": ">", - } - ] - }, - include=["static"], - q="q", - threshold=0, - ) - assert_matches_type(ProfileResponse, client, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_profile(self, async_client: AsyncSupermemory) -> None: - response = await async_client.with_raw_response.profile( - container_tag="containerTag", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - client = await response.parse() - assert_matches_type(ProfileResponse, client, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_profile(self, async_client: AsyncSupermemory) -> None: - async with async_client.with_streaming_response.profile( - container_tag="containerTag", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - client = await response.parse() - assert_matches_type(ProfileResponse, client, path=["response"]) - - assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_connections.py b/tests/api_resources/test_connections.py deleted file mode 100644 index 3eb33fcb..00000000 --- a/tests/api_resources/test_connections.py +++ /dev/null @@ -1,904 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from supermemory import Supermemory, AsyncSupermemory -from tests.utils import assert_matches_type -from supermemory.types import ( - ConnectionListResponse, - ConnectionCreateResponse, - ConnectionGetByIDResponse, - ConnectionGetByTagResponse, - ConnectionConfigureResponse, - ConnectionResourcesResponse, - ConnectionDeleteByIDResponse, - ConnectionListDocumentsResponse, - ConnectionDeleteByProviderResponse, -) - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestConnections: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_create(self, client: Supermemory) -> None: - connection = client.connections.create( - provider="notion", - ) - assert_matches_type(ConnectionCreateResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_create_with_all_params(self, client: Supermemory) -> None: - connection = client.connections.create( - provider="notion", - container_tag="containerTag", - container_tags=["_:_k--W2K_1V"], - document_limit=1, - metadata={"foo": "string"}, - redirect_url="redirectUrl", - ) - assert_matches_type(ConnectionCreateResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_create(self, client: Supermemory) -> None: - response = client.connections.with_raw_response.create( - provider="notion", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - connection = response.parse() - assert_matches_type(ConnectionCreateResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_create(self, client: Supermemory) -> None: - with client.connections.with_streaming_response.create( - provider="notion", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - connection = response.parse() - assert_matches_type(ConnectionCreateResponse, connection, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_list(self, client: Supermemory) -> None: - connection = client.connections.list() - assert_matches_type(ConnectionListResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_list_with_all_params(self, client: Supermemory) -> None: - connection = client.connections.list( - container_tags=["user_123", "project_123"], - ) - assert_matches_type(ConnectionListResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_list(self, client: Supermemory) -> None: - response = client.connections.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - connection = response.parse() - assert_matches_type(ConnectionListResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_list(self, client: Supermemory) -> None: - with client.connections.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - connection = response.parse() - assert_matches_type(ConnectionListResponse, connection, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_configure(self, client: Supermemory) -> None: - connection = client.connections.configure( - connection_id="connectionId", - resources=[{"foo": "bar"}], - ) - assert_matches_type(ConnectionConfigureResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_configure(self, client: Supermemory) -> None: - response = client.connections.with_raw_response.configure( - connection_id="connectionId", - resources=[{"foo": "bar"}], - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - connection = response.parse() - assert_matches_type(ConnectionConfigureResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_configure(self, client: Supermemory) -> None: - with client.connections.with_streaming_response.configure( - connection_id="connectionId", - resources=[{"foo": "bar"}], - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - connection = response.parse() - assert_matches_type(ConnectionConfigureResponse, connection, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_path_params_configure(self, client: Supermemory) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `connection_id` but received ''"): - client.connections.with_raw_response.configure( - connection_id="", - resources=[{"foo": "bar"}], - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_delete_by_id(self, client: Supermemory) -> None: - connection = client.connections.delete_by_id( - connection_id="connectionId", - ) - assert_matches_type(ConnectionDeleteByIDResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_delete_by_id_with_all_params(self, client: Supermemory) -> None: - connection = client.connections.delete_by_id( - connection_id="connectionId", - delete_documents="deleteDocuments", - ) - assert_matches_type(ConnectionDeleteByIDResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_delete_by_id(self, client: Supermemory) -> None: - response = client.connections.with_raw_response.delete_by_id( - connection_id="connectionId", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - connection = response.parse() - assert_matches_type(ConnectionDeleteByIDResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_delete_by_id(self, client: Supermemory) -> None: - with client.connections.with_streaming_response.delete_by_id( - connection_id="connectionId", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - connection = response.parse() - assert_matches_type(ConnectionDeleteByIDResponse, connection, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_path_params_delete_by_id(self, client: Supermemory) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `connection_id` but received ''"): - client.connections.with_raw_response.delete_by_id( - connection_id="", - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_delete_by_provider(self, client: Supermemory) -> None: - connection = client.connections.delete_by_provider( - provider="notion", - container_tags=["user_123", "project_123"], - ) - assert_matches_type(ConnectionDeleteByProviderResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_delete_by_provider(self, client: Supermemory) -> None: - response = client.connections.with_raw_response.delete_by_provider( - provider="notion", - container_tags=["user_123", "project_123"], - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - connection = response.parse() - assert_matches_type(ConnectionDeleteByProviderResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_delete_by_provider(self, client: Supermemory) -> None: - with client.connections.with_streaming_response.delete_by_provider( - provider="notion", - container_tags=["user_123", "project_123"], - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - connection = response.parse() - assert_matches_type(ConnectionDeleteByProviderResponse, connection, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_get_by_id(self, client: Supermemory) -> None: - connection = client.connections.get_by_id( - "connectionId", - ) - assert_matches_type(ConnectionGetByIDResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_get_by_id(self, client: Supermemory) -> None: - response = client.connections.with_raw_response.get_by_id( - "connectionId", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - connection = response.parse() - assert_matches_type(ConnectionGetByIDResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_get_by_id(self, client: Supermemory) -> None: - with client.connections.with_streaming_response.get_by_id( - "connectionId", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - connection = response.parse() - assert_matches_type(ConnectionGetByIDResponse, connection, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_path_params_get_by_id(self, client: Supermemory) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `connection_id` but received ''"): - client.connections.with_raw_response.get_by_id( - "", - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_get_by_tag(self, client: Supermemory) -> None: - connection = client.connections.get_by_tag( - provider="notion", - container_tags=["user_123", "project_123"], - ) - assert_matches_type(ConnectionGetByTagResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_get_by_tag(self, client: Supermemory) -> None: - response = client.connections.with_raw_response.get_by_tag( - provider="notion", - container_tags=["user_123", "project_123"], - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - connection = response.parse() - assert_matches_type(ConnectionGetByTagResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_get_by_tag(self, client: Supermemory) -> None: - with client.connections.with_streaming_response.get_by_tag( - provider="notion", - container_tags=["user_123", "project_123"], - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - connection = response.parse() - assert_matches_type(ConnectionGetByTagResponse, connection, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_import(self, client: Supermemory) -> None: - connection = client.connections.import_( - provider="notion", - ) - assert_matches_type(str, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_import_with_all_params(self, client: Supermemory) -> None: - connection = client.connections.import_( - provider="notion", - container_tags=["user_123", "project_123"], - ) - assert_matches_type(str, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_import(self, client: Supermemory) -> None: - response = client.connections.with_raw_response.import_( - provider="notion", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - connection = response.parse() - assert_matches_type(str, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_import(self, client: Supermemory) -> None: - with client.connections.with_streaming_response.import_( - provider="notion", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - connection = response.parse() - assert_matches_type(str, connection, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_list_documents(self, client: Supermemory) -> None: - connection = client.connections.list_documents( - provider="notion", - ) - assert_matches_type(ConnectionListDocumentsResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_list_documents_with_all_params(self, client: Supermemory) -> None: - connection = client.connections.list_documents( - provider="notion", - container_tags=["user_123", "project_123"], - ) - assert_matches_type(ConnectionListDocumentsResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_list_documents(self, client: Supermemory) -> None: - response = client.connections.with_raw_response.list_documents( - provider="notion", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - connection = response.parse() - assert_matches_type(ConnectionListDocumentsResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_list_documents(self, client: Supermemory) -> None: - with client.connections.with_streaming_response.list_documents( - provider="notion", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - connection = response.parse() - assert_matches_type(ConnectionListDocumentsResponse, connection, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_resources(self, client: Supermemory) -> None: - connection = client.connections.resources( - connection_id="connectionId", - ) - assert_matches_type(ConnectionResourcesResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_resources_with_all_params(self, client: Supermemory) -> None: - connection = client.connections.resources( - connection_id="connectionId", - page=0, - parent_id="parent_id", - per_page=0, - ) - assert_matches_type(ConnectionResourcesResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_resources(self, client: Supermemory) -> None: - response = client.connections.with_raw_response.resources( - connection_id="connectionId", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - connection = response.parse() - assert_matches_type(ConnectionResourcesResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_resources(self, client: Supermemory) -> None: - with client.connections.with_streaming_response.resources( - connection_id="connectionId", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - connection = response.parse() - assert_matches_type(ConnectionResourcesResponse, connection, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_path_params_resources(self, client: Supermemory) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `connection_id` but received ''"): - client.connections.with_raw_response.resources( - connection_id="", - ) - - -class TestAsyncConnections: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_create(self, async_client: AsyncSupermemory) -> None: - connection = await async_client.connections.create( - provider="notion", - ) - assert_matches_type(ConnectionCreateResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_create_with_all_params(self, async_client: AsyncSupermemory) -> None: - connection = await async_client.connections.create( - provider="notion", - container_tag="containerTag", - container_tags=["_:_k--W2K_1V"], - document_limit=1, - metadata={"foo": "string"}, - redirect_url="redirectUrl", - ) - assert_matches_type(ConnectionCreateResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_create(self, async_client: AsyncSupermemory) -> None: - response = await async_client.connections.with_raw_response.create( - provider="notion", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - connection = await response.parse() - assert_matches_type(ConnectionCreateResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_create(self, async_client: AsyncSupermemory) -> None: - async with async_client.connections.with_streaming_response.create( - provider="notion", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - connection = await response.parse() - assert_matches_type(ConnectionCreateResponse, connection, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_list(self, async_client: AsyncSupermemory) -> None: - connection = await async_client.connections.list() - assert_matches_type(ConnectionListResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_list_with_all_params(self, async_client: AsyncSupermemory) -> None: - connection = await async_client.connections.list( - container_tags=["user_123", "project_123"], - ) - assert_matches_type(ConnectionListResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_list(self, async_client: AsyncSupermemory) -> None: - response = await async_client.connections.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - connection = await response.parse() - assert_matches_type(ConnectionListResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_list(self, async_client: AsyncSupermemory) -> None: - async with async_client.connections.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - connection = await response.parse() - assert_matches_type(ConnectionListResponse, connection, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_configure(self, async_client: AsyncSupermemory) -> None: - connection = await async_client.connections.configure( - connection_id="connectionId", - resources=[{"foo": "bar"}], - ) - assert_matches_type(ConnectionConfigureResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_configure(self, async_client: AsyncSupermemory) -> None: - response = await async_client.connections.with_raw_response.configure( - connection_id="connectionId", - resources=[{"foo": "bar"}], - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - connection = await response.parse() - assert_matches_type(ConnectionConfigureResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_configure(self, async_client: AsyncSupermemory) -> None: - async with async_client.connections.with_streaming_response.configure( - connection_id="connectionId", - resources=[{"foo": "bar"}], - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - connection = await response.parse() - assert_matches_type(ConnectionConfigureResponse, connection, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_path_params_configure(self, async_client: AsyncSupermemory) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `connection_id` but received ''"): - await async_client.connections.with_raw_response.configure( - connection_id="", - resources=[{"foo": "bar"}], - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_delete_by_id(self, async_client: AsyncSupermemory) -> None: - connection = await async_client.connections.delete_by_id( - connection_id="connectionId", - ) - assert_matches_type(ConnectionDeleteByIDResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_delete_by_id_with_all_params(self, async_client: AsyncSupermemory) -> None: - connection = await async_client.connections.delete_by_id( - connection_id="connectionId", - delete_documents="deleteDocuments", - ) - assert_matches_type(ConnectionDeleteByIDResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_delete_by_id(self, async_client: AsyncSupermemory) -> None: - response = await async_client.connections.with_raw_response.delete_by_id( - connection_id="connectionId", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - connection = await response.parse() - assert_matches_type(ConnectionDeleteByIDResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_delete_by_id(self, async_client: AsyncSupermemory) -> None: - async with async_client.connections.with_streaming_response.delete_by_id( - connection_id="connectionId", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - connection = await response.parse() - assert_matches_type(ConnectionDeleteByIDResponse, connection, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_path_params_delete_by_id(self, async_client: AsyncSupermemory) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `connection_id` but received ''"): - await async_client.connections.with_raw_response.delete_by_id( - connection_id="", - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_delete_by_provider(self, async_client: AsyncSupermemory) -> None: - connection = await async_client.connections.delete_by_provider( - provider="notion", - container_tags=["user_123", "project_123"], - ) - assert_matches_type(ConnectionDeleteByProviderResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_delete_by_provider(self, async_client: AsyncSupermemory) -> None: - response = await async_client.connections.with_raw_response.delete_by_provider( - provider="notion", - container_tags=["user_123", "project_123"], - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - connection = await response.parse() - assert_matches_type(ConnectionDeleteByProviderResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_delete_by_provider(self, async_client: AsyncSupermemory) -> None: - async with async_client.connections.with_streaming_response.delete_by_provider( - provider="notion", - container_tags=["user_123", "project_123"], - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - connection = await response.parse() - assert_matches_type(ConnectionDeleteByProviderResponse, connection, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_get_by_id(self, async_client: AsyncSupermemory) -> None: - connection = await async_client.connections.get_by_id( - "connectionId", - ) - assert_matches_type(ConnectionGetByIDResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_get_by_id(self, async_client: AsyncSupermemory) -> None: - response = await async_client.connections.with_raw_response.get_by_id( - "connectionId", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - connection = await response.parse() - assert_matches_type(ConnectionGetByIDResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_get_by_id(self, async_client: AsyncSupermemory) -> None: - async with async_client.connections.with_streaming_response.get_by_id( - "connectionId", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - connection = await response.parse() - assert_matches_type(ConnectionGetByIDResponse, connection, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_path_params_get_by_id(self, async_client: AsyncSupermemory) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `connection_id` but received ''"): - await async_client.connections.with_raw_response.get_by_id( - "", - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_get_by_tag(self, async_client: AsyncSupermemory) -> None: - connection = await async_client.connections.get_by_tag( - provider="notion", - container_tags=["user_123", "project_123"], - ) - assert_matches_type(ConnectionGetByTagResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_get_by_tag(self, async_client: AsyncSupermemory) -> None: - response = await async_client.connections.with_raw_response.get_by_tag( - provider="notion", - container_tags=["user_123", "project_123"], - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - connection = await response.parse() - assert_matches_type(ConnectionGetByTagResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_get_by_tag(self, async_client: AsyncSupermemory) -> None: - async with async_client.connections.with_streaming_response.get_by_tag( - provider="notion", - container_tags=["user_123", "project_123"], - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - connection = await response.parse() - assert_matches_type(ConnectionGetByTagResponse, connection, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_import(self, async_client: AsyncSupermemory) -> None: - connection = await async_client.connections.import_( - provider="notion", - ) - assert_matches_type(str, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_import_with_all_params(self, async_client: AsyncSupermemory) -> None: - connection = await async_client.connections.import_( - provider="notion", - container_tags=["user_123", "project_123"], - ) - assert_matches_type(str, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_import(self, async_client: AsyncSupermemory) -> None: - response = await async_client.connections.with_raw_response.import_( - provider="notion", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - connection = await response.parse() - assert_matches_type(str, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_import(self, async_client: AsyncSupermemory) -> None: - async with async_client.connections.with_streaming_response.import_( - provider="notion", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - connection = await response.parse() - assert_matches_type(str, connection, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_list_documents(self, async_client: AsyncSupermemory) -> None: - connection = await async_client.connections.list_documents( - provider="notion", - ) - assert_matches_type(ConnectionListDocumentsResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_list_documents_with_all_params(self, async_client: AsyncSupermemory) -> None: - connection = await async_client.connections.list_documents( - provider="notion", - container_tags=["user_123", "project_123"], - ) - assert_matches_type(ConnectionListDocumentsResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_list_documents(self, async_client: AsyncSupermemory) -> None: - response = await async_client.connections.with_raw_response.list_documents( - provider="notion", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - connection = await response.parse() - assert_matches_type(ConnectionListDocumentsResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_list_documents(self, async_client: AsyncSupermemory) -> None: - async with async_client.connections.with_streaming_response.list_documents( - provider="notion", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - connection = await response.parse() - assert_matches_type(ConnectionListDocumentsResponse, connection, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_resources(self, async_client: AsyncSupermemory) -> None: - connection = await async_client.connections.resources( - connection_id="connectionId", - ) - assert_matches_type(ConnectionResourcesResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_resources_with_all_params(self, async_client: AsyncSupermemory) -> None: - connection = await async_client.connections.resources( - connection_id="connectionId", - page=0, - parent_id="parent_id", - per_page=0, - ) - assert_matches_type(ConnectionResourcesResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_resources(self, async_client: AsyncSupermemory) -> None: - response = await async_client.connections.with_raw_response.resources( - connection_id="connectionId", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - connection = await response.parse() - assert_matches_type(ConnectionResourcesResponse, connection, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_resources(self, async_client: AsyncSupermemory) -> None: - async with async_client.connections.with_streaming_response.resources( - connection_id="connectionId", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - connection = await response.parse() - assert_matches_type(ConnectionResourcesResponse, connection, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_path_params_resources(self, async_client: AsyncSupermemory) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `connection_id` but received ''"): - await async_client.connections.with_raw_response.resources( - connection_id="", - ) diff --git a/tests/api_resources/test_documents.py b/tests/api_resources/test_documents.py deleted file mode 100644 index cf5e869f..00000000 --- a/tests/api_resources/test_documents.py +++ /dev/null @@ -1,1001 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from supermemory import Supermemory, AsyncSupermemory -from tests.utils import assert_matches_type -from supermemory.types import ( - DocumentAddResponse, - DocumentGetResponse, - DocumentListResponse, - DocumentUpdateResponse, - DocumentBatchAddResponse, - DocumentDeleteBulkResponse, - DocumentUploadFileResponse, - DocumentListProcessingResponse, -) - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestDocuments: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_update(self, client: Supermemory) -> None: - document = client.documents.update( - id="id", - ) - assert_matches_type(DocumentUpdateResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_update_with_all_params(self, client: Supermemory) -> None: - document = client.documents.update( - id="id", - container_tag="user_alex", - container_tags=["_:_k--W2K_1V"], - content="Our API rate limits are 100 req/min on free and 1000 on pro. Clients should use exponential backoff on 429s.", - custom_id="doc-api-rate-limits", - document_date="2025-03-14", - filepath="/documents/reports/file.pdf", - filter_by_metadata={ - "department": "engineering", - "region": "us", - }, - metadata={ - "source": "upload", - "language": "en", - }, - task_type="memory", - ) - assert_matches_type(DocumentUpdateResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_update(self, client: Supermemory) -> None: - response = client.documents.with_raw_response.update( - id="id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = response.parse() - assert_matches_type(DocumentUpdateResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_update(self, client: Supermemory) -> None: - with client.documents.with_streaming_response.update( - id="id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = response.parse() - assert_matches_type(DocumentUpdateResponse, document, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_path_params_update(self, client: Supermemory) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.documents.with_raw_response.update( - id="", - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_list(self, client: Supermemory) -> None: - document = client.documents.list() - assert_matches_type(DocumentListResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_list_with_all_params(self, client: Supermemory) -> None: - document = client.documents.list( - container_tags=["_:_k--W2K_1V"], - filepath="filepath", - filters={ - "and_": [ - { - "key": "group", - "value": "jira_users", - "filter_type": "metadata", - "ignore_case": "true", - "negate": False, - "numeric_operator": ">", - }, - { - "key": "timestamp", - "value": "1742745777", - "filter_type": "numeric", - "ignore_case": "true", - "negate": False, - "numeric_operator": ">", - }, - ] - }, - include_content=False, - limit=10, - order="desc", - page=1, - sort="createdAt", - ) - assert_matches_type(DocumentListResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_list(self, client: Supermemory) -> None: - response = client.documents.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = response.parse() - assert_matches_type(DocumentListResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_list(self, client: Supermemory) -> None: - with client.documents.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = response.parse() - assert_matches_type(DocumentListResponse, document, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_delete(self, client: Supermemory) -> None: - document = client.documents.delete( - "id", - ) - assert document is None - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_delete(self, client: Supermemory) -> None: - response = client.documents.with_raw_response.delete( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = response.parse() - assert document is None - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_delete(self, client: Supermemory) -> None: - with client.documents.with_streaming_response.delete( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = response.parse() - assert document is None - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_path_params_delete(self, client: Supermemory) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.documents.with_raw_response.delete( - "", - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_add(self, client: Supermemory) -> None: - document = client.documents.add( - content="content", - ) - assert_matches_type(DocumentAddResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_add_with_all_params(self, client: Supermemory) -> None: - document = client.documents.add( - content="content", - container_tag="containerTag", - container_tags=["string"], - custom_id="customId", - document_date="documentDate", - dreaming="instant", - entity_context="entityContext", - filepath="filepath", - filter_by_metadata={"foo": "string"}, - metadata={"foo": "string"}, - task_type="memory", - ) - assert_matches_type(DocumentAddResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_add(self, client: Supermemory) -> None: - response = client.documents.with_raw_response.add( - content="content", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = response.parse() - assert_matches_type(DocumentAddResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_add(self, client: Supermemory) -> None: - with client.documents.with_streaming_response.add( - content="content", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = response.parse() - assert_matches_type(DocumentAddResponse, document, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_batch_add(self, client: Supermemory) -> None: - document = client.documents.batch_add( - documents=[ - { - "content": "Our API rate limits are 100 req/min on free and 1000 on pro. Clients should use exponential backoff on 429s." - } - ], - ) - assert_matches_type(DocumentBatchAddResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_batch_add_with_all_params(self, client: Supermemory) -> None: - document = client.documents.batch_add( - documents=[ - { - "content": "Our API rate limits are 100 req/min on free and 1000 on pro. Clients should use exponential backoff on 429s.", - "container_tag": "user_alex", - "container_tags": ["_:_k--W2K_1V"], - "custom_id": "doc-api-rate-limits", - "document_date": "2025-03-14", - "dreaming": "instant", - "entity_context": "User's name is {XYZ}", - "filepath": "/documents/reports/file.pdf", - "filter_by_metadata": { - "department": "engineering", - "region": "us", - }, - "metadata": { - "source": "upload", - "language": "en", - }, - "task_type": "memory", - } - ], - container_tag="user_alex", - container_tags=["_:_k--W2K_1V"], - content=None, - document_date="2025-03-14", - dreaming="instant", - entity_context="User's name is {XYZ}", - filepath="/documents/reports/file.pdf", - filter_by_metadata={ - "department": "engineering", - "region": "us", - }, - metadata={ - "source": "upload", - "language": "en", - }, - task_type="memory", - ) - assert_matches_type(DocumentBatchAddResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_batch_add(self, client: Supermemory) -> None: - response = client.documents.with_raw_response.batch_add( - documents=[ - { - "content": "Our API rate limits are 100 req/min on free and 1000 on pro. Clients should use exponential backoff on 429s." - } - ], - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = response.parse() - assert_matches_type(DocumentBatchAddResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_batch_add(self, client: Supermemory) -> None: - with client.documents.with_streaming_response.batch_add( - documents=[ - { - "content": "Our API rate limits are 100 req/min on free and 1000 on pro. Clients should use exponential backoff on 429s." - } - ], - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = response.parse() - assert_matches_type(DocumentBatchAddResponse, document, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_delete_bulk(self, client: Supermemory) -> None: - document = client.documents.delete_bulk() - assert_matches_type(DocumentDeleteBulkResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_delete_bulk_with_all_params(self, client: Supermemory) -> None: - document = client.documents.delete_bulk( - container_tags=["_:_k--W2K_1V"], - filepath="filepath", - ids=["acxV5LHMEsG2hMSNb4umbn", "bxcV5LHMEsG2hMSNb4umbn"], - ) - assert_matches_type(DocumentDeleteBulkResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_delete_bulk(self, client: Supermemory) -> None: - response = client.documents.with_raw_response.delete_bulk() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = response.parse() - assert_matches_type(DocumentDeleteBulkResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_delete_bulk(self, client: Supermemory) -> None: - with client.documents.with_streaming_response.delete_bulk() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = response.parse() - assert_matches_type(DocumentDeleteBulkResponse, document, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_get(self, client: Supermemory) -> None: - document = client.documents.get( - "id", - ) - assert_matches_type(DocumentGetResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_get(self, client: Supermemory) -> None: - response = client.documents.with_raw_response.get( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = response.parse() - assert_matches_type(DocumentGetResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_get(self, client: Supermemory) -> None: - with client.documents.with_streaming_response.get( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = response.parse() - assert_matches_type(DocumentGetResponse, document, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_path_params_get(self, client: Supermemory) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.documents.with_raw_response.get( - "", - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_list_processing(self, client: Supermemory) -> None: - document = client.documents.list_processing() - assert_matches_type(DocumentListProcessingResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_list_processing_with_all_params(self, client: Supermemory) -> None: - document = client.documents.list_processing( - container_tags="user_123,sm_project_default", - limit="50", - page="1", - view="all", - ) - assert_matches_type(DocumentListProcessingResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_list_processing(self, client: Supermemory) -> None: - response = client.documents.with_raw_response.list_processing() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = response.parse() - assert_matches_type(DocumentListProcessingResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_list_processing(self, client: Supermemory) -> None: - with client.documents.with_streaming_response.list_processing() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = response.parse() - assert_matches_type(DocumentListProcessingResponse, document, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_upload_file(self, client: Supermemory) -> None: - document = client.documents.upload_file( - file=b"Example data", - ) - assert_matches_type(DocumentUploadFileResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_upload_file_with_all_params(self, client: Supermemory) -> None: - document = client.documents.upload_file( - file=b"Example data", - container_tag="user", - container_tags="containerTags", - custom_id="mem_abc123", - dreaming="instant", - entity_context="User's name is {XYZ}", - filepath="/documents/reports/file.pdf", - file_type="image", - filter_by_metadata='{"department": "engineering"}', - metadata='{"category": "technology", "isPublic": true, "readingTime": 5}', - mime_type="mimeType", - task_type="memory", - use_advanced_processing="true", - ) - assert_matches_type(DocumentUploadFileResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_upload_file(self, client: Supermemory) -> None: - response = client.documents.with_raw_response.upload_file( - file=b"Example data", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = response.parse() - assert_matches_type(DocumentUploadFileResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_upload_file(self, client: Supermemory) -> None: - with client.documents.with_streaming_response.upload_file( - file=b"Example data", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = response.parse() - assert_matches_type(DocumentUploadFileResponse, document, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncDocuments: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_update(self, async_client: AsyncSupermemory) -> None: - document = await async_client.documents.update( - id="id", - ) - assert_matches_type(DocumentUpdateResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_update_with_all_params(self, async_client: AsyncSupermemory) -> None: - document = await async_client.documents.update( - id="id", - container_tag="user_alex", - container_tags=["_:_k--W2K_1V"], - content="Our API rate limits are 100 req/min on free and 1000 on pro. Clients should use exponential backoff on 429s.", - custom_id="doc-api-rate-limits", - document_date="2025-03-14", - filepath="/documents/reports/file.pdf", - filter_by_metadata={ - "department": "engineering", - "region": "us", - }, - metadata={ - "source": "upload", - "language": "en", - }, - task_type="memory", - ) - assert_matches_type(DocumentUpdateResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_update(self, async_client: AsyncSupermemory) -> None: - response = await async_client.documents.with_raw_response.update( - id="id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = await response.parse() - assert_matches_type(DocumentUpdateResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_update(self, async_client: AsyncSupermemory) -> None: - async with async_client.documents.with_streaming_response.update( - id="id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = await response.parse() - assert_matches_type(DocumentUpdateResponse, document, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_path_params_update(self, async_client: AsyncSupermemory) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.documents.with_raw_response.update( - id="", - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_list(self, async_client: AsyncSupermemory) -> None: - document = await async_client.documents.list() - assert_matches_type(DocumentListResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_list_with_all_params(self, async_client: AsyncSupermemory) -> None: - document = await async_client.documents.list( - container_tags=["_:_k--W2K_1V"], - filepath="filepath", - filters={ - "and_": [ - { - "key": "group", - "value": "jira_users", - "filter_type": "metadata", - "ignore_case": "true", - "negate": False, - "numeric_operator": ">", - }, - { - "key": "timestamp", - "value": "1742745777", - "filter_type": "numeric", - "ignore_case": "true", - "negate": False, - "numeric_operator": ">", - }, - ] - }, - include_content=False, - limit=10, - order="desc", - page=1, - sort="createdAt", - ) - assert_matches_type(DocumentListResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_list(self, async_client: AsyncSupermemory) -> None: - response = await async_client.documents.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = await response.parse() - assert_matches_type(DocumentListResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_list(self, async_client: AsyncSupermemory) -> None: - async with async_client.documents.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = await response.parse() - assert_matches_type(DocumentListResponse, document, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_delete(self, async_client: AsyncSupermemory) -> None: - document = await async_client.documents.delete( - "id", - ) - assert document is None - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_delete(self, async_client: AsyncSupermemory) -> None: - response = await async_client.documents.with_raw_response.delete( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = await response.parse() - assert document is None - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_delete(self, async_client: AsyncSupermemory) -> None: - async with async_client.documents.with_streaming_response.delete( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = await response.parse() - assert document is None - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_path_params_delete(self, async_client: AsyncSupermemory) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.documents.with_raw_response.delete( - "", - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_add(self, async_client: AsyncSupermemory) -> None: - document = await async_client.documents.add( - content="content", - ) - assert_matches_type(DocumentAddResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_add_with_all_params(self, async_client: AsyncSupermemory) -> None: - document = await async_client.documents.add( - content="content", - container_tag="containerTag", - container_tags=["string"], - custom_id="customId", - document_date="documentDate", - dreaming="instant", - entity_context="entityContext", - filepath="filepath", - filter_by_metadata={"foo": "string"}, - metadata={"foo": "string"}, - task_type="memory", - ) - assert_matches_type(DocumentAddResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_add(self, async_client: AsyncSupermemory) -> None: - response = await async_client.documents.with_raw_response.add( - content="content", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = await response.parse() - assert_matches_type(DocumentAddResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_add(self, async_client: AsyncSupermemory) -> None: - async with async_client.documents.with_streaming_response.add( - content="content", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = await response.parse() - assert_matches_type(DocumentAddResponse, document, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_batch_add(self, async_client: AsyncSupermemory) -> None: - document = await async_client.documents.batch_add( - documents=[ - { - "content": "Our API rate limits are 100 req/min on free and 1000 on pro. Clients should use exponential backoff on 429s." - } - ], - ) - assert_matches_type(DocumentBatchAddResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_batch_add_with_all_params(self, async_client: AsyncSupermemory) -> None: - document = await async_client.documents.batch_add( - documents=[ - { - "content": "Our API rate limits are 100 req/min on free and 1000 on pro. Clients should use exponential backoff on 429s.", - "container_tag": "user_alex", - "container_tags": ["_:_k--W2K_1V"], - "custom_id": "doc-api-rate-limits", - "document_date": "2025-03-14", - "dreaming": "instant", - "entity_context": "User's name is {XYZ}", - "filepath": "/documents/reports/file.pdf", - "filter_by_metadata": { - "department": "engineering", - "region": "us", - }, - "metadata": { - "source": "upload", - "language": "en", - }, - "task_type": "memory", - } - ], - container_tag="user_alex", - container_tags=["_:_k--W2K_1V"], - content=None, - document_date="2025-03-14", - dreaming="instant", - entity_context="User's name is {XYZ}", - filepath="/documents/reports/file.pdf", - filter_by_metadata={ - "department": "engineering", - "region": "us", - }, - metadata={ - "source": "upload", - "language": "en", - }, - task_type="memory", - ) - assert_matches_type(DocumentBatchAddResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_batch_add(self, async_client: AsyncSupermemory) -> None: - response = await async_client.documents.with_raw_response.batch_add( - documents=[ - { - "content": "Our API rate limits are 100 req/min on free and 1000 on pro. Clients should use exponential backoff on 429s." - } - ], - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = await response.parse() - assert_matches_type(DocumentBatchAddResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_batch_add(self, async_client: AsyncSupermemory) -> None: - async with async_client.documents.with_streaming_response.batch_add( - documents=[ - { - "content": "Our API rate limits are 100 req/min on free and 1000 on pro. Clients should use exponential backoff on 429s." - } - ], - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = await response.parse() - assert_matches_type(DocumentBatchAddResponse, document, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_delete_bulk(self, async_client: AsyncSupermemory) -> None: - document = await async_client.documents.delete_bulk() - assert_matches_type(DocumentDeleteBulkResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_delete_bulk_with_all_params(self, async_client: AsyncSupermemory) -> None: - document = await async_client.documents.delete_bulk( - container_tags=["_:_k--W2K_1V"], - filepath="filepath", - ids=["acxV5LHMEsG2hMSNb4umbn", "bxcV5LHMEsG2hMSNb4umbn"], - ) - assert_matches_type(DocumentDeleteBulkResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_delete_bulk(self, async_client: AsyncSupermemory) -> None: - response = await async_client.documents.with_raw_response.delete_bulk() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = await response.parse() - assert_matches_type(DocumentDeleteBulkResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_delete_bulk(self, async_client: AsyncSupermemory) -> None: - async with async_client.documents.with_streaming_response.delete_bulk() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = await response.parse() - assert_matches_type(DocumentDeleteBulkResponse, document, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_get(self, async_client: AsyncSupermemory) -> None: - document = await async_client.documents.get( - "id", - ) - assert_matches_type(DocumentGetResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_get(self, async_client: AsyncSupermemory) -> None: - response = await async_client.documents.with_raw_response.get( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = await response.parse() - assert_matches_type(DocumentGetResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_get(self, async_client: AsyncSupermemory) -> None: - async with async_client.documents.with_streaming_response.get( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = await response.parse() - assert_matches_type(DocumentGetResponse, document, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_path_params_get(self, async_client: AsyncSupermemory) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.documents.with_raw_response.get( - "", - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_list_processing(self, async_client: AsyncSupermemory) -> None: - document = await async_client.documents.list_processing() - assert_matches_type(DocumentListProcessingResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_list_processing_with_all_params(self, async_client: AsyncSupermemory) -> None: - document = await async_client.documents.list_processing( - container_tags="user_123,sm_project_default", - limit="50", - page="1", - view="all", - ) - assert_matches_type(DocumentListProcessingResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_list_processing(self, async_client: AsyncSupermemory) -> None: - response = await async_client.documents.with_raw_response.list_processing() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = await response.parse() - assert_matches_type(DocumentListProcessingResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_list_processing(self, async_client: AsyncSupermemory) -> None: - async with async_client.documents.with_streaming_response.list_processing() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = await response.parse() - assert_matches_type(DocumentListProcessingResponse, document, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_upload_file(self, async_client: AsyncSupermemory) -> None: - document = await async_client.documents.upload_file( - file=b"Example data", - ) - assert_matches_type(DocumentUploadFileResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_upload_file_with_all_params(self, async_client: AsyncSupermemory) -> None: - document = await async_client.documents.upload_file( - file=b"Example data", - container_tag="user", - container_tags="containerTags", - custom_id="mem_abc123", - dreaming="instant", - entity_context="User's name is {XYZ}", - filepath="/documents/reports/file.pdf", - file_type="image", - filter_by_metadata='{"department": "engineering"}', - metadata='{"category": "technology", "isPublic": true, "readingTime": 5}', - mime_type="mimeType", - task_type="memory", - use_advanced_processing="true", - ) - assert_matches_type(DocumentUploadFileResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_upload_file(self, async_client: AsyncSupermemory) -> None: - response = await async_client.documents.with_raw_response.upload_file( - file=b"Example data", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - document = await response.parse() - assert_matches_type(DocumentUploadFileResponse, document, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_upload_file(self, async_client: AsyncSupermemory) -> None: - async with async_client.documents.with_streaming_response.upload_file( - file=b"Example data", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - document = await response.parse() - assert_matches_type(DocumentUploadFileResponse, document, path=["response"]) - - assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_memories.py b/tests/api_resources/test_memories.py deleted file mode 100644 index 59574552..00000000 --- a/tests/api_resources/test_memories.py +++ /dev/null @@ -1,227 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from supermemory import Supermemory, AsyncSupermemory -from tests.utils import assert_matches_type -from supermemory.types import ( - MemoryForgetResponse, - MemoryUpdateMemoryResponse, -) - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestMemories: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_forget(self, client: Supermemory) -> None: - memory = client.memories.forget( - container_tag="user_123", - ) - assert_matches_type(MemoryForgetResponse, memory, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_forget_with_all_params(self, client: Supermemory) -> None: - memory = client.memories.forget( - container_tag="user_123", - id="mem_abc123", - content="John prefers dark mode", - reason="outdated information", - ) - assert_matches_type(MemoryForgetResponse, memory, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_forget(self, client: Supermemory) -> None: - response = client.memories.with_raw_response.forget( - container_tag="user_123", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - memory = response.parse() - assert_matches_type(MemoryForgetResponse, memory, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_forget(self, client: Supermemory) -> None: - with client.memories.with_streaming_response.forget( - container_tag="user_123", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - memory = response.parse() - assert_matches_type(MemoryForgetResponse, memory, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_update_memory(self, client: Supermemory) -> None: - memory = client.memories.update_memory( - container_tag="user_123", - new_content="John now prefers light mode", - ) - assert_matches_type(MemoryUpdateMemoryResponse, memory, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_update_memory_with_all_params(self, client: Supermemory) -> None: - memory = client.memories.update_memory( - container_tag="user_123", - new_content="John now prefers light mode", - id="mem_abc123", - content="John prefers dark mode", - forget_after="2026-06-01T00:00:00Z", - forget_reason="temporary project deadline", - metadata={"foo": "string"}, - temporal_context={ - "document_date": "documentDate", - "event_date": ["string"], - }, - ) - assert_matches_type(MemoryUpdateMemoryResponse, memory, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_update_memory(self, client: Supermemory) -> None: - response = client.memories.with_raw_response.update_memory( - container_tag="user_123", - new_content="John now prefers light mode", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - memory = response.parse() - assert_matches_type(MemoryUpdateMemoryResponse, memory, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_update_memory(self, client: Supermemory) -> None: - with client.memories.with_streaming_response.update_memory( - container_tag="user_123", - new_content="John now prefers light mode", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - memory = response.parse() - assert_matches_type(MemoryUpdateMemoryResponse, memory, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncMemories: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_forget(self, async_client: AsyncSupermemory) -> None: - memory = await async_client.memories.forget( - container_tag="user_123", - ) - assert_matches_type(MemoryForgetResponse, memory, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_forget_with_all_params(self, async_client: AsyncSupermemory) -> None: - memory = await async_client.memories.forget( - container_tag="user_123", - id="mem_abc123", - content="John prefers dark mode", - reason="outdated information", - ) - assert_matches_type(MemoryForgetResponse, memory, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_forget(self, async_client: AsyncSupermemory) -> None: - response = await async_client.memories.with_raw_response.forget( - container_tag="user_123", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - memory = await response.parse() - assert_matches_type(MemoryForgetResponse, memory, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_forget(self, async_client: AsyncSupermemory) -> None: - async with async_client.memories.with_streaming_response.forget( - container_tag="user_123", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - memory = await response.parse() - assert_matches_type(MemoryForgetResponse, memory, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_update_memory(self, async_client: AsyncSupermemory) -> None: - memory = await async_client.memories.update_memory( - container_tag="user_123", - new_content="John now prefers light mode", - ) - assert_matches_type(MemoryUpdateMemoryResponse, memory, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_update_memory_with_all_params(self, async_client: AsyncSupermemory) -> None: - memory = await async_client.memories.update_memory( - container_tag="user_123", - new_content="John now prefers light mode", - id="mem_abc123", - content="John prefers dark mode", - forget_after="2026-06-01T00:00:00Z", - forget_reason="temporary project deadline", - metadata={"foo": "string"}, - temporal_context={ - "document_date": "documentDate", - "event_date": ["string"], - }, - ) - assert_matches_type(MemoryUpdateMemoryResponse, memory, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_update_memory(self, async_client: AsyncSupermemory) -> None: - response = await async_client.memories.with_raw_response.update_memory( - container_tag="user_123", - new_content="John now prefers light mode", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - memory = await response.parse() - assert_matches_type(MemoryUpdateMemoryResponse, memory, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_update_memory(self, async_client: AsyncSupermemory) -> None: - async with async_client.memories.with_streaming_response.update_memory( - container_tag="user_123", - new_content="John now prefers light mode", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - memory = await response.parse() - assert_matches_type(MemoryUpdateMemoryResponse, memory, 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 deleted file mode 100644 index a6a922b2..00000000 --- a/tests/api_resources/test_search.py +++ /dev/null @@ -1,436 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from supermemory import Supermemory, AsyncSupermemory -from tests.utils import assert_matches_type -from supermemory.types import ( - SearchExecuteResponse, - SearchMemoriesResponse, - SearchDocumentsResponse, -) - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestSearch: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_documents(self, client: Supermemory) -> None: - search = client.search.documents( - q="what are the API rate limits", - ) - assert_matches_type(SearchDocumentsResponse, search, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_documents_with_all_params(self, client: Supermemory) -> None: - search = client.search.documents( - q="what are the API rate limits", - categories_filter=["string"], - chunk_threshold=0.5, - container_tag="user_alex", - container_tags=["_:_k--W2K_1V"], - doc_id="docId", - document_threshold=0, - filepath="filepath", - filters={ - "or_": [ - { - "key": "key", - "value": "value", - "filter_type": "metadata", - "ignore_case": "true", - "negate": "true", - "numeric_operator": ">", - } - ] - }, - include_full_docs=False, - include_summary=True, - limit=10, - only_matching_chunks=True, - rerank=False, - rewrite_query=False, - ) - assert_matches_type(SearchDocumentsResponse, search, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_documents(self, client: Supermemory) -> None: - response = client.search.with_raw_response.documents( - q="what are the API rate limits", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - search = response.parse() - assert_matches_type(SearchDocumentsResponse, search, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_documents(self, client: Supermemory) -> None: - with client.search.with_streaming_response.documents( - q="what are the API rate limits", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - search = response.parse() - assert_matches_type(SearchDocumentsResponse, search, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_execute(self, client: Supermemory) -> None: - search = client.search.execute( - q="what are the API rate limits", - ) - assert_matches_type(SearchExecuteResponse, search, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_execute_with_all_params(self, client: Supermemory) -> None: - search = client.search.execute( - q="what are the API rate limits", - categories_filter=["string"], - chunk_threshold=0.5, - container_tag="user_alex", - container_tags=["_:_k--W2K_1V"], - doc_id="docId", - document_threshold=0, - filepath="filepath", - filters={ - "or_": [ - { - "key": "key", - "value": "value", - "filter_type": "metadata", - "ignore_case": "true", - "negate": "true", - "numeric_operator": ">", - } - ] - }, - include_full_docs=False, - include_summary=True, - limit=10, - only_matching_chunks=True, - rerank=False, - rewrite_query=False, - ) - assert_matches_type(SearchExecuteResponse, search, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_execute(self, client: Supermemory) -> None: - response = client.search.with_raw_response.execute( - q="what are the API rate limits", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - search = response.parse() - assert_matches_type(SearchExecuteResponse, search, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_execute(self, client: Supermemory) -> None: - with client.search.with_streaming_response.execute( - q="what are the API rate limits", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - search = response.parse() - assert_matches_type(SearchExecuteResponse, search, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_memories(self, client: Supermemory) -> None: - search = client.search.memories( - q="what are the API rate limits", - ) - assert_matches_type(SearchMemoriesResponse, search, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_memories_with_all_params(self, client: Supermemory) -> None: - search = client.search.memories( - q="what are the API rate limits", - aggregate=False, - container_tag="user_alex", - container_tags=["user_alex"], - filepath="filepath", - filters={ - "or_": [ - { - "key": "key", - "value": "value", - "filter_type": "metadata", - "ignore_case": "true", - "negate": "true", - "numeric_operator": ">", - } - ] - }, - include={ - "chunks": False, - "documents": True, - "forgotten_memories": False, - "related_memories": True, - "summaries": True, - }, - limit=10, - rerank=False, - rewrite_query=False, - search_mode="memories", - threshold=0.5, - ) - assert_matches_type(SearchMemoriesResponse, search, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_memories(self, client: Supermemory) -> None: - response = client.search.with_raw_response.memories( - q="what are the API rate limits", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - search = response.parse() - assert_matches_type(SearchMemoriesResponse, search, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_memories(self, client: Supermemory) -> None: - with client.search.with_streaming_response.memories( - q="what are the API rate limits", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - search = response.parse() - assert_matches_type(SearchMemoriesResponse, search, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncSearch: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_documents(self, async_client: AsyncSupermemory) -> None: - search = await async_client.search.documents( - q="what are the API rate limits", - ) - assert_matches_type(SearchDocumentsResponse, search, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_documents_with_all_params(self, async_client: AsyncSupermemory) -> None: - search = await async_client.search.documents( - q="what are the API rate limits", - categories_filter=["string"], - chunk_threshold=0.5, - container_tag="user_alex", - container_tags=["_:_k--W2K_1V"], - doc_id="docId", - document_threshold=0, - filepath="filepath", - filters={ - "or_": [ - { - "key": "key", - "value": "value", - "filter_type": "metadata", - "ignore_case": "true", - "negate": "true", - "numeric_operator": ">", - } - ] - }, - include_full_docs=False, - include_summary=True, - limit=10, - only_matching_chunks=True, - rerank=False, - rewrite_query=False, - ) - assert_matches_type(SearchDocumentsResponse, search, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_documents(self, async_client: AsyncSupermemory) -> None: - response = await async_client.search.with_raw_response.documents( - q="what are the API rate limits", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - search = await response.parse() - assert_matches_type(SearchDocumentsResponse, search, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_documents(self, async_client: AsyncSupermemory) -> None: - async with async_client.search.with_streaming_response.documents( - q="what are the API rate limits", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - search = await response.parse() - assert_matches_type(SearchDocumentsResponse, search, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_execute(self, async_client: AsyncSupermemory) -> None: - search = await async_client.search.execute( - q="what are the API rate limits", - ) - assert_matches_type(SearchExecuteResponse, search, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_execute_with_all_params(self, async_client: AsyncSupermemory) -> None: - search = await async_client.search.execute( - q="what are the API rate limits", - categories_filter=["string"], - chunk_threshold=0.5, - container_tag="user_alex", - container_tags=["_:_k--W2K_1V"], - doc_id="docId", - document_threshold=0, - filepath="filepath", - filters={ - "or_": [ - { - "key": "key", - "value": "value", - "filter_type": "metadata", - "ignore_case": "true", - "negate": "true", - "numeric_operator": ">", - } - ] - }, - include_full_docs=False, - include_summary=True, - limit=10, - only_matching_chunks=True, - rerank=False, - rewrite_query=False, - ) - assert_matches_type(SearchExecuteResponse, search, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_execute(self, async_client: AsyncSupermemory) -> None: - response = await async_client.search.with_raw_response.execute( - q="what are the API rate limits", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - search = await response.parse() - assert_matches_type(SearchExecuteResponse, search, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_execute(self, async_client: AsyncSupermemory) -> None: - async with async_client.search.with_streaming_response.execute( - q="what are the API rate limits", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - search = await response.parse() - assert_matches_type(SearchExecuteResponse, search, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_memories(self, async_client: AsyncSupermemory) -> None: - search = await async_client.search.memories( - q="what are the API rate limits", - ) - assert_matches_type(SearchMemoriesResponse, search, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_memories_with_all_params(self, async_client: AsyncSupermemory) -> None: - search = await async_client.search.memories( - q="what are the API rate limits", - aggregate=False, - container_tag="user_alex", - container_tags=["user_alex"], - filepath="filepath", - filters={ - "or_": [ - { - "key": "key", - "value": "value", - "filter_type": "metadata", - "ignore_case": "true", - "negate": "true", - "numeric_operator": ">", - } - ] - }, - include={ - "chunks": False, - "documents": True, - "forgotten_memories": False, - "related_memories": True, - "summaries": True, - }, - limit=10, - rerank=False, - rewrite_query=False, - search_mode="memories", - threshold=0.5, - ) - assert_matches_type(SearchMemoriesResponse, search, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_memories(self, async_client: AsyncSupermemory) -> None: - response = await async_client.search.with_raw_response.memories( - q="what are the API rate limits", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - search = await response.parse() - assert_matches_type(SearchMemoriesResponse, search, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_memories(self, async_client: AsyncSupermemory) -> None: - async with async_client.search.with_streaming_response.memories( - q="what are the API rate limits", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - search = await response.parse() - assert_matches_type(SearchMemoriesResponse, search, path=["response"]) - - assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_settings.py b/tests/api_resources/test_settings.py deleted file mode 100644 index ce41b4f5..00000000 --- a/tests/api_resources/test_settings.py +++ /dev/null @@ -1,196 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from supermemory import Supermemory, AsyncSupermemory -from tests.utils import assert_matches_type -from supermemory.types import SettingGetResponse, SettingUpdateResponse - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestSettings: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_update(self, client: Supermemory) -> None: - setting = client.settings.update() - assert_matches_type(SettingUpdateResponse, setting, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_update_with_all_params(self, client: Supermemory) -> None: - setting = client.settings.update( - chunk_size=-2147483648, - exclude_items="string", - filter_prompt="filterPrompt", - github_client_id="githubClientId", - github_client_secret="githubClientSecret", - github_custom_key_enabled=True, - google_drive_client_id="googleDriveClientId", - google_drive_client_secret="googleDriveClientSecret", - google_drive_custom_key_enabled=True, - include_items="string", - notion_client_id="notionClientId", - notion_client_secret="notionClientSecret", - notion_custom_key_enabled=True, - onedrive_client_id="onedriveClientId", - onedrive_client_secret="onedriveClientSecret", - onedrive_custom_key_enabled=True, - profile_buckets=[ - { - "key": "key", - "description": "description", - } - ], - should_llm_filter=True, - ) - assert_matches_type(SettingUpdateResponse, setting, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_update(self, client: Supermemory) -> None: - response = client.settings.with_raw_response.update() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - setting = response.parse() - assert_matches_type(SettingUpdateResponse, setting, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_update(self, client: Supermemory) -> None: - with client.settings.with_streaming_response.update() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - setting = response.parse() - assert_matches_type(SettingUpdateResponse, setting, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_get(self, client: Supermemory) -> None: - setting = client.settings.get() - assert_matches_type(SettingGetResponse, setting, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_get(self, client: Supermemory) -> None: - response = client.settings.with_raw_response.get() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - setting = response.parse() - assert_matches_type(SettingGetResponse, setting, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_get(self, client: Supermemory) -> None: - with client.settings.with_streaming_response.get() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - setting = response.parse() - assert_matches_type(SettingGetResponse, setting, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncSettings: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_update(self, async_client: AsyncSupermemory) -> None: - setting = await async_client.settings.update() - assert_matches_type(SettingUpdateResponse, setting, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_update_with_all_params(self, async_client: AsyncSupermemory) -> None: - setting = await async_client.settings.update( - chunk_size=-2147483648, - exclude_items="string", - filter_prompt="filterPrompt", - github_client_id="githubClientId", - github_client_secret="githubClientSecret", - github_custom_key_enabled=True, - google_drive_client_id="googleDriveClientId", - google_drive_client_secret="googleDriveClientSecret", - google_drive_custom_key_enabled=True, - include_items="string", - notion_client_id="notionClientId", - notion_client_secret="notionClientSecret", - notion_custom_key_enabled=True, - onedrive_client_id="onedriveClientId", - onedrive_client_secret="onedriveClientSecret", - onedrive_custom_key_enabled=True, - profile_buckets=[ - { - "key": "key", - "description": "description", - } - ], - should_llm_filter=True, - ) - assert_matches_type(SettingUpdateResponse, setting, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_update(self, async_client: AsyncSupermemory) -> None: - response = await async_client.settings.with_raw_response.update() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - setting = await response.parse() - assert_matches_type(SettingUpdateResponse, setting, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_update(self, async_client: AsyncSupermemory) -> None: - async with async_client.settings.with_streaming_response.update() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - setting = await response.parse() - assert_matches_type(SettingUpdateResponse, setting, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_get(self, async_client: AsyncSupermemory) -> None: - setting = await async_client.settings.get() - assert_matches_type(SettingGetResponse, setting, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_get(self, async_client: AsyncSupermemory) -> None: - response = await async_client.settings.with_raw_response.get() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - setting = await response.parse() - assert_matches_type(SettingGetResponse, setting, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_get(self, async_client: AsyncSupermemory) -> None: - async with async_client.settings.with_streaming_response.get() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - setting = await response.parse() - assert_matches_type(SettingGetResponse, setting, path=["response"]) - - assert cast(Any, response.is_closed) is True diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 2baaab81..00000000 --- a/tests/conftest.py +++ /dev/null @@ -1,84 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -import logging -from typing import TYPE_CHECKING, Iterator, AsyncIterator - -import httpx -import pytest -from pytest_asyncio import is_async_test - -from supermemory import Supermemory, AsyncSupermemory, DefaultAioHttpClient -from supermemory._utils import is_dict - -if TYPE_CHECKING: - from _pytest.fixtures import FixtureRequest # pyright: ignore[reportPrivateImportUsage] - -pytest.register_assert_rewrite("tests.utils") - -logging.getLogger("supermemory").setLevel(logging.DEBUG) - - -# automatically add `pytest.mark.asyncio()` to all of our async tests -# so we don't have to add that boilerplate everywhere -def pytest_collection_modifyitems(items: list[pytest.Function]) -> None: - pytest_asyncio_tests = (item for item in items if is_async_test(item)) - session_scope_marker = pytest.mark.asyncio(loop_scope="session") - for async_test in pytest_asyncio_tests: - async_test.add_marker(session_scope_marker, append=False) - - # We skip tests that use both the aiohttp client and respx_mock as respx_mock - # doesn't support custom transports. - for item in items: - if "async_client" not in item.fixturenames or "respx_mock" not in item.fixturenames: - continue - - if not hasattr(item, "callspec"): - continue - - async_client_param = item.callspec.params.get("async_client") - if is_dict(async_client_param) and async_client_param.get("http_client") == "aiohttp": - item.add_marker(pytest.mark.skip(reason="aiohttp client is not compatible with respx_mock")) - - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - -api_key = "My API Key" - - -@pytest.fixture(scope="session") -def client(request: FixtureRequest) -> Iterator[Supermemory]: - strict = getattr(request, "param", True) - if not isinstance(strict, bool): - raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") - - with Supermemory(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: - yield client - - -@pytest.fixture(scope="session") -async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncSupermemory]: - param = getattr(request, "param", True) - - # defaults - strict = True - http_client: None | httpx.AsyncClient = None - - if isinstance(param, bool): - strict = param - elif is_dict(param): - strict = param.get("strict", True) - assert isinstance(strict, bool) - - http_client_type = param.get("http_client", "httpx") - if http_client_type == "aiohttp": - http_client = DefaultAioHttpClient() - else: - raise TypeError(f"Unexpected fixture parameter type {type(param)}, expected bool or dict") - - async with AsyncSupermemory( - base_url=base_url, api_key=api_key, _strict_response_validation=strict, http_client=http_client - ) as client: - yield client diff --git a/tests/live/test_live.py b/tests/live/test_live.py new file mode 100644 index 00000000..86dbce1a --- /dev/null +++ b/tests/live/test_live.py @@ -0,0 +1,84 @@ +"""End-to-end checks against the real API. + +Skipped unless SUPERMEMORY_LIVE_API_KEY is set; each run uses a throwaway +namespace that it deletes afterwards. + + SUPERMEMORY_LIVE_API_KEY=... uv run --group dev pytest tests/live -q +""" + +import os +import time +import uuid +from collections.abc import Iterator + +import pytest + +import supermemory +from supermemory import AsyncSupermemory, Supermemory + +API_KEY = os.environ.get("SUPERMEMORY_LIVE_API_KEY") +BASE_URL = os.environ.get("SUPERMEMORY_LIVE_BASE_URL") +pytestmark = pytest.mark.skipif(not API_KEY, reason="SUPERMEMORY_LIVE_API_KEY not set") + + +@pytest.fixture(scope="module") +def client() -> Supermemory: + return Supermemory(api_key=API_KEY, base_url=BASE_URL) + + +@pytest.fixture(scope="module") +def namespace(client: Supermemory) -> Iterator[str]: + name = f"sdk_python_test_{uuid.uuid4().hex[:10]}" + yield name + try: + client.namespaces.delete(name) + except supermemory.APIStatusError: + pass + + +def test_add_get_search_list(client: Supermemory, namespace: str) -> None: + added = client.add(namespace, content="The SDK test user's favorite color is teal.", id="favorite-color") + assert added.id + + deadline = time.monotonic() + 90 + while True: + document = client.documents.get(namespace, added.id) + if document.system.status in ("done", "failed") or time.monotonic() > deadline: + break + time.sleep(3) + assert document.system.status == "done", document.system.status + + results = client.search(namespace, query="What color does the user like?", limit=5).results + assert results, "expected at least one search result" + + page = client.list(namespace, "documents", limit=10) + assert any(d.id == added.id for d in page.documents or []) + + assert namespace in {ns.id for ns in client.namespaces.list()} + + +def test_profile_and_buckets(client: Supermemory, namespace: str) -> None: + client.profile(namespace) + client.profiles.get_buckets(namespace) + + +def test_errors(client: Supermemory, namespace: str) -> None: + with pytest.raises(supermemory.NotFoundError) as exc: + client.documents.get(namespace, "does-not-exist") + assert exc.value.status_code == 404 + assert exc.value.response is not None + + with pytest.raises(supermemory.AuthenticationError): + Supermemory(api_key="sm_invalid", base_url=BASE_URL).namespaces.list() + + +def test_raw_response_and_per_call_options(client: Supermemory, namespace: str) -> None: + raw = client.with_raw_response.search(namespace, query="color", extra_headers={"X-Sdk-Test": "1"}, timeout=30) + assert raw.status_code == 200 + assert isinstance(raw.parse().results, list) + + +async def test_async_client(namespace: str) -> None: + async with AsyncSupermemory(api_key=API_KEY, base_url=BASE_URL) as client: + response = await client.search(namespace, query="color") + assert isinstance(response.results, list) diff --git a/tests/sample_file.txt b/tests/sample_file.txt deleted file mode 100644 index af5626b4..00000000 --- a/tests/sample_file.txt +++ /dev/null @@ -1 +0,0 @@ -Hello, world! diff --git a/tests/test_client.py b/tests/test_client.py index 6b2b631c..c1279ac3 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,1980 +1,150 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +"""Wire-level checks for the generated client: routes, auth, bodies, errors. -from __future__ import annotations +Requests go through an httpx MockTransport, so these run offline and pin the +SDK surface that scripts/generate must keep producing. +""" -import gc -import os -import sys import json -import asyncio -import inspect -import dataclasses -import tracemalloc -from typing import Any, Union, TypeVar, Callable, Iterable, Iterator, Optional, Coroutine, cast -from unittest import mock -from typing_extensions import Literal, AsyncIterator, override +from collections.abc import Callable +from typing import Any import httpx import pytest -from respx import MockRouter -from pydantic import ValidationError -from supermemory import Supermemory, AsyncSupermemory, APIResponseValidationError -from supermemory._types import Omit -from supermemory._utils import asyncify -from supermemory._models import BaseModel, FinalRequestOptions -from supermemory._exceptions import APIStatusError, APITimeoutError, SupermemoryError, APIResponseValidationError -from supermemory._base_client import ( - DEFAULT_TIMEOUT, - HTTPX_DEFAULT_TIMEOUT, - BaseClient, - OtherPlatform, - DefaultHttpxClient, - DefaultAsyncHttpxClient, - get_platform, - make_request_options, -) +from supermemory import AsyncSupermemory, Supermemory +from supermemory.core.api_error import ApiError +from supermemory.errors import NotFoundError -from .utils import update_env +Captured = list[httpx.Request] -T = TypeVar("T") -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") -api_key = "My API Key" +def _responder(captured: Captured, status: int = 200, body: Any = None) -> Callable[[httpx.Request], httpx.Response]: + def handle(request: httpx.Request) -> httpx.Response: + captured.append(request) + return httpx.Response(status, json={} if body is None else body) -def _get_params(client: BaseClient[Any, Any]) -> dict[str, str]: - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - url = httpx.URL(request.url) - return dict(url.params) + return handle -def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float: - return 0.1 - - -def mirror_request_content(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, content=request.content) - - -# note: we can't use the httpx.MockTransport class as it consumes the request -# body itself, which means we can't test that the body is read lazily -class MockTransport(httpx.BaseTransport, httpx.AsyncBaseTransport): - def __init__( - self, - handler: Callable[[httpx.Request], httpx.Response] - | Callable[[httpx.Request], Coroutine[Any, Any, httpx.Response]], - ) -> None: - self.handler = handler - - @override - def handle_request( - self, - request: httpx.Request, - ) -> httpx.Response: - assert not inspect.iscoroutinefunction(self.handler), "handler must not be a coroutine function" - assert inspect.isfunction(self.handler), "handler must be a function" - return self.handler(request) - - @override - async def handle_async_request( - self, - request: httpx.Request, - ) -> httpx.Response: - assert inspect.iscoroutinefunction(self.handler), "handler must be a coroutine function" - return await self.handler(request) - - -@dataclasses.dataclass -class Counter: - value: int = 0 - - -def _make_sync_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> Iterator[T]: - for item in iterable: - if counter: - counter.value += 1 - yield item - - -async def _make_async_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> AsyncIterator[T]: - for item in iterable: - if counter: - counter.value += 1 - yield item - - -def _get_open_connections(client: Supermemory | AsyncSupermemory) -> int: - transport = client._client._transport - assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) - - pool = transport._pool - return len(pool._requests) - - -class TestSupermemory: - @pytest.mark.respx(base_url=base_url) - 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 = 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, client: Supermemory) -> None: - respx_mock.post("/foo").mock( - return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') - ) - - 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, client: Supermemory) -> None: - copied = client.copy() - assert id(copied) != id(client) - - copied = client.copy(api_key="another My API Key") - assert copied.api_key == "another My API Key" - assert client.api_key == "My API Key" - - def test_copy_default_options(self, client: Supermemory) -> None: - # options that have a default are overridden correctly - copied = client.copy(max_retries=7) - assert copied.max_retries == 7 - assert client.max_retries == 2 - - copied2 = copied.copy(max_retries=6) - assert copied2.max_retries == 6 - assert copied.max_retries == 7 - - # timeout - assert isinstance(client.timeout, httpx.Timeout) - copied = client.copy(timeout=None) - assert copied.timeout is None - assert isinstance(client.timeout, httpx.Timeout) - - def test_copy_default_headers(self) -> None: - client = Supermemory( - base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} - ) - assert client.default_headers["X-Foo"] == "bar" - - # does not override the already given value when not specified - copied = client.copy() - assert copied.default_headers["X-Foo"] == "bar" - - # merges already given headers - copied = client.copy(default_headers={"X-Bar": "stainless"}) - assert copied.default_headers["X-Foo"] == "bar" - assert copied.default_headers["X-Bar"] == "stainless" - - # uses new values for any already given headers - copied = client.copy(default_headers={"X-Foo": "stainless"}) - assert copied.default_headers["X-Foo"] == "stainless" - - # set_default_headers - - # completely overrides already set values - copied = client.copy(set_default_headers={}) - assert copied.default_headers.get("X-Foo") is None - - copied = client.copy(set_default_headers={"X-Bar": "Robert"}) - assert copied.default_headers["X-Bar"] == "Robert" - - with pytest.raises( - ValueError, - 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( - base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} - ) - assert _get_params(client)["foo"] == "bar" - - # does not override the already given value when not specified - copied = client.copy() - assert _get_params(copied)["foo"] == "bar" - - # merges already given params - copied = client.copy(default_query={"bar": "stainless"}) - params = _get_params(copied) - assert params["foo"] == "bar" - assert params["bar"] == "stainless" - - # uses new values for any already given headers - copied = client.copy(default_query={"foo": "stainless"}) - assert _get_params(copied)["foo"] == "stainless" - - # set_default_query - - # completely overrides already set values - copied = client.copy(set_default_query={}) - assert _get_params(copied) == {} - - copied = client.copy(set_default_query={"bar": "Robert"}) - assert _get_params(copied)["bar"] == "Robert" - - with pytest.raises( - ValueError, - # TODO: update - match="`default_query` and `set_default_query` arguments are mutually exclusive", - ): - client.copy(set_default_query={}, default_query={"foo": "Bar"}) - - 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. - client.__init__, # type: ignore[misc] - ) - copy_signature = inspect.signature(client.copy) - exclude_params = {"transport", "proxies", "_strict_response_validation"} - - for name in init_signature.parameters.keys(): - if name in exclude_params: - continue - - copy_param = copy_signature.parameters.get(name) - assert copy_param is not None, f"copy() signature is missing the {name} param" - - @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") - def test_copy_build_request(self, client: Supermemory) -> None: - options = FinalRequestOptions(method="get", url="/foo") - - def build_request(options: FinalRequestOptions) -> None: - client_copy = client.copy() - client_copy._build_request(options) - - # ensure that the machinery is warmed up before tracing starts. - build_request(options) - gc.collect() - - tracemalloc.start(1000) - - snapshot_before = tracemalloc.take_snapshot() - - ITERATIONS = 10 - for _ in range(ITERATIONS): - build_request(options) - - gc.collect() - snapshot_after = tracemalloc.take_snapshot() - - tracemalloc.stop() - - def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None: - if diff.count == 0: - # Avoid false positives by considering only leaks (i.e. allocations that persist). - return - - if diff.count % ITERATIONS != 0: - # Avoid false positives by considering only leaks that appear per iteration. - return - - for frame in diff.traceback: - if any( - frame.filename.endswith(fragment) - for fragment in [ - # to_raw_response_wrapper leaks through the @functools.wraps() decorator. - # - # removing the decorator fixes the leak for reasons we don't understand. - "supermemory/_legacy_response.py", - "supermemory/_response.py", - # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. - "supermemory/_compat.py", - # Standard library leaks we don't care about. - "/logging/__init__.py", - ] - ): - return - - leaks.append(diff) - - leaks: list[tracemalloc.StatisticDiff] = [] - for diff in snapshot_after.compare_to(snapshot_before, "traceback"): - add_leak(leaks, diff) - if leaks: - for leak in leaks: - print("MEMORY LEAK:", leak) - for frame in leak.traceback: - print(frame) - raise AssertionError() - - 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 = 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) - - def test_client_timeout_option(self) -> None: - client = Supermemory( - base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) - ) - - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - 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: - client = Supermemory( - base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client - ) - - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - 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( - base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client - ) - - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - 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( - base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client - ) - - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - 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: - Supermemory( - base_url=base_url, - api_key=api_key, - _strict_response_validation=True, - http_client=cast(Any, http_client), - ) - - def test_default_headers_option(self) -> None: - test_client = Supermemory( - base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} - ) - 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" - - test_client2 = Supermemory( - base_url=base_url, - api_key=api_key, - _strict_response_validation=True, - default_headers={ - "X-Foo": "stainless", - "X-Stainless-Lang": "my-overriding-header", - }, - ) - 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")) - assert request.headers.get("Authorization") == f"Bearer {api_key}" - - with pytest.raises(SupermemoryError): - with update_env(**{"SUPERMEMORY_API_KEY": Omit()}): - client2 = Supermemory(base_url=base_url, api_key=None, _strict_response_validation=True) - _ = client2 - - def test_default_query_option(self) -> None: - client = Supermemory( - base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} - ) - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - url = httpx.URL(request.url) - assert dict(url.params) == {"query_param": "bar"} - - request = client._build_request( - FinalRequestOptions( - method="get", - url="/foo", - params={"foo": "baz", "query_param": "overridden"}, - ) - ) - url = httpx.URL(request.url) - assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} - - client.close() - - def test_hardcoded_query_params_in_url(self, client: Supermemory) -> None: - request = client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) - url = httpx.URL(request.url) - assert dict(url.params) == {"beta": "true"} - - request = client._build_request( - FinalRequestOptions( - method="get", - url="/foo?beta=true", - params={"limit": "10", "page": "abc"}, - ) - ) - url = httpx.URL(request.url) - assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} - - request = client._build_request( - FinalRequestOptions( - method="get", - url="/files/a%2Fb?beta=true", - params={"limit": "10"}, - ) - ) - assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" - - def test_request_extra_json(self, client: Supermemory) -> None: - request = client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - json_data={"foo": "bar"}, - extra_json={"baz": False}, - ), - ) - data = json.loads(request.content.decode("utf-8")) - assert data == {"foo": "bar", "baz": False} - - request = client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - extra_json={"baz": False}, - ), - ) - data = json.loads(request.content.decode("utf-8")) - assert data == {"baz": False} - - # `extra_json` takes priority over `json_data` when keys clash - request = client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - json_data={"foo": "bar", "baz": True}, - extra_json={"baz": None}, - ), - ) - data = json.loads(request.content.decode("utf-8")) - assert data == {"foo": "bar", "baz": None} - - def test_request_extra_headers(self, client: Supermemory) -> None: - request = client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - **make_request_options(extra_headers={"X-Foo": "Foo"}), - ), - ) - assert request.headers.get("X-Foo") == "Foo" - - # `extra_headers` takes priority over `default_headers` when keys clash - request = client.with_options(default_headers={"X-Bar": "true"})._build_request( - FinalRequestOptions( - method="post", - url="/foo", - **make_request_options( - extra_headers={"X-Bar": "false"}, - ), - ), - ) - assert request.headers.get("X-Bar") == "false" - - def test_request_extra_query(self, client: Supermemory) -> None: - request = client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - **make_request_options( - extra_query={"my_query_param": "Foo"}, - ), - ), - ) - params = dict(request.url.params) - assert params == {"my_query_param": "Foo"} - - # if both `query` and `extra_query` are given, they are merged - request = client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - **make_request_options( - query={"bar": "1"}, - extra_query={"foo": "2"}, - ), - ), - ) - params = dict(request.url.params) - assert params == {"bar": "1", "foo": "2"} - - # `extra_query` takes priority over `query` when keys clash - request = client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - **make_request_options( - query={"foo": "1"}, - extra_query={"foo": "2"}, - ), - ), - ) - params = dict(request.url.params) - assert params == {"foo": "2"} - - def test_multipart_repeating_array(self, client: Supermemory) -> None: - request = client._build_request( - FinalRequestOptions.construct( - method="post", - url="/foo", - headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, - json_data={"array": ["foo", "bar"]}, - files=[("foo.txt", b"hello world")], - ) - ) - - assert request.read().split(b"\r\n") == [ - b"--6b7ba517decee4a450543ea6ae821c82", - b'Content-Disposition: form-data; name="array[]"', - b"", - b"foo", - b"--6b7ba517decee4a450543ea6ae821c82", - b'Content-Disposition: form-data; name="array[]"', - b"", - b"bar", - b"--6b7ba517decee4a450543ea6ae821c82", - b'Content-Disposition: form-data; name="foo.txt"; filename="upload"', - b"Content-Type: application/octet-stream", - b"", - b"hello world", - b"--6b7ba517decee4a450543ea6ae821c82--", - b"", - ] - - @pytest.mark.respx(base_url=base_url) - def test_binary_content_upload(self, respx_mock: MockRouter, client: Supermemory) -> None: - respx_mock.post("/upload").mock(side_effect=mirror_request_content) - - file_content = b"Hello, this is a test file." - - response = client.post( - "/upload", - content=file_content, - cast_to=httpx.Response, - options={"headers": {"Content-Type": "application/octet-stream"}}, - ) - - assert response.status_code == 200 - assert response.request.headers["Content-Type"] == "application/octet-stream" - assert response.content == file_content - - def test_binary_content_upload_with_iterator(self) -> None: - file_content = b"Hello, this is a test file." - counter = Counter() - iterator = _make_sync_iterator([file_content], counter=counter) - - def mock_handler(request: httpx.Request) -> httpx.Response: - assert counter.value == 0, "the request body should not have been read" - return httpx.Response(200, content=request.read()) - - with Supermemory( - base_url=base_url, - api_key=api_key, - _strict_response_validation=True, - http_client=httpx.Client(transport=MockTransport(handler=mock_handler)), - ) as client: - response = client.post( - "/upload", - content=iterator, - cast_to=httpx.Response, - options={"headers": {"Content-Type": "application/octet-stream"}}, - ) - - assert response.status_code == 200 - assert response.request.headers["Content-Type"] == "application/octet-stream" - assert response.content == file_content - assert counter.value == 1 - - @pytest.mark.respx(base_url=base_url) - def test_binary_content_upload_with_body_is_deprecated(self, respx_mock: MockRouter, client: Supermemory) -> None: - respx_mock.post("/upload").mock(side_effect=mirror_request_content) - - file_content = b"Hello, this is a test file." - - with pytest.deprecated_call( - match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." - ): - response = client.post( - "/upload", - body=file_content, - cast_to=httpx.Response, - options={"headers": {"Content-Type": "application/octet-stream"}}, - ) - - assert response.status_code == 200 - assert response.request.headers["Content-Type"] == "application/octet-stream" - assert response.content == file_content - - @pytest.mark.respx(base_url=base_url) - def test_basic_union_response(self, respx_mock: MockRouter, client: Supermemory) -> None: - class Model1(BaseModel): - name: str - - class Model2(BaseModel): - foo: str - - respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - - 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, client: Supermemory) -> None: - """Union of objects with the same field name using a different type""" - - class Model1(BaseModel): - foo: int - - class Model2(BaseModel): - foo: str - - respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - - 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 = 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, client: Supermemory) -> None: - """ - Response that sets Content-Type to something other than application/json but returns json data - """ - - class Model(BaseModel): - foo: int - - respx_mock.get("/foo").mock( - return_value=httpx.Response( - 200, - content=json.dumps({"foo": 2}), - headers={"Content-Type": "application/text"}, - ) - ) - - response = client.get("/foo", cast_to=Model) - assert isinstance(response, Model) - assert response.foo == 2 - - def test_base_url_setter(self) -> None: - client = Supermemory( - base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True - ) - assert client.base_url == "https://example.com/from_init/" - - client.base_url = "https://example.com/from_setter" # type: ignore[assignment] - - 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) - assert client.base_url == "http://localhost:5000/from/env/" - - @pytest.mark.parametrize( - "client", - [ - Supermemory( - base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True - ), - Supermemory( - base_url="http://localhost:5000/custom/path/", - api_key=api_key, - _strict_response_validation=True, - http_client=httpx.Client(), - ), - ], - ids=["standard", "custom http client"], - ) - def test_base_url_trailing_slash(self, client: Supermemory) -> None: - request = client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - json_data={"foo": "bar"}, - ), - ) - assert request.url == "http://localhost:5000/custom/path/foo" - client.close() - - @pytest.mark.parametrize( - "client", - [ - Supermemory( - base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True - ), - Supermemory( - base_url="http://localhost:5000/custom/path/", - api_key=api_key, - _strict_response_validation=True, - http_client=httpx.Client(), - ), - ], - ids=["standard", "custom http client"], - ) - def test_base_url_no_trailing_slash(self, client: Supermemory) -> None: - request = client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - json_data={"foo": "bar"}, - ), - ) - assert request.url == "http://localhost:5000/custom/path/foo" - client.close() - - @pytest.mark.parametrize( - "client", - [ - Supermemory( - base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True - ), - Supermemory( - base_url="http://localhost:5000/custom/path/", - api_key=api_key, - _strict_response_validation=True, - http_client=httpx.Client(), - ), - ], - ids=["standard", "custom http client"], +def _client(captured: Captured, **kwargs: Any) -> Supermemory: + return Supermemory( + api_key="sm_test", + httpx_client=httpx.Client(transport=httpx.MockTransport(_responder(captured, **kwargs))), ) - def test_absolute_request_url(self, client: Supermemory) -> None: - request = client._build_request( - FinalRequestOptions( - method="post", - url="https://myapi.com/foo", - json_data={"foo": "bar"}, - ), - ) - assert request.url == "https://myapi.com/foo" - client.close() - - def test_copied_client_does_not_close_http(self) -> None: - test_client = Supermemory(base_url=base_url, api_key=api_key, _strict_response_validation=True) - assert not test_client.is_closed() - - copied = test_client.copy() - assert copied is not test_client - - del copied - - assert not test_client.is_closed() - - def test_client_context_manager(self) -> None: - 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 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, 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: - client.get("/foo", cast_to=Model) - - assert isinstance(exc.value.__cause__, ValidationError) - - def test_client_max_retries_validation(self) -> None: - with pytest.raises(TypeError, match=r"max_retries cannot be None"): - Supermemory( - base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None) - ) - - @pytest.mark.respx(base_url=base_url) - def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: - class Model(BaseModel): - name: str - - respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) - - strict_client = Supermemory(base_url=base_url, api_key=api_key, _strict_response_validation=True) - - with pytest.raises(APIResponseValidationError): - strict_client.get("/foo", cast_to=Model) - - non_strict_client = Supermemory(base_url=base_url, api_key=api_key, _strict_response_validation=False) - - 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", - [ - [3, "20", 20], - [3, "0", 0.5], - [3, "-10", 0.5], - [3, "60", 60], - [3, "61", 0.5], - [3, "Fri, 29 Sep 2023 16:26:57 GMT", 20], - [3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5], - [3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5], - [3, "Fri, 29 Sep 2023 16:27:37 GMT", 60], - [3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5], - [3, "99999999999999999999999999999999999", 0.5], - [3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5], - [3, "", 0.5], - [2, "", 0.5 * 2.0], - [1, "", 0.5 * 4.0], - [-1100, "", 8], # test large number potentially overflowing - ], - ) - @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) - 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) - assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] - - @mock.patch("supermemory._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) - @pytest.mark.respx(base_url=base_url) - def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: Supermemory) -> None: - respx_mock.post("/v3/documents").mock(side_effect=httpx.TimeoutException("Test timeout error")) - - with pytest.raises(APITimeoutError): - client.with_streaming_response.add(content="content").__enter__() - - 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) - def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: Supermemory) -> None: - respx_mock.post("/v3/documents").mock(return_value=httpx.Response(500)) - - with pytest.raises(APIStatusError): - client.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) - @pytest.mark.respx(base_url=base_url) - @pytest.mark.parametrize("failure_mode", ["status", "exception"]) - def test_retries_taken( - self, - client: Supermemory, - failures_before_success: int, - failure_mode: Literal["status", "exception"], - respx_mock: MockRouter, - ) -> None: - client = client.with_options(max_retries=4) - - nb_retries = 0 - - def retry_handler(_request: httpx.Request) -> httpx.Response: - nonlocal nb_retries - if nb_retries < failures_before_success: - nb_retries += 1 - if failure_mode == "exception": - raise RuntimeError("oops") - return httpx.Response(500) - return httpx.Response(200) - - respx_mock.post("/v3/documents").mock(side_effect=retry_handler) - - response = client.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 - - @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) - def test_omit_retry_count_header( - self, client: Supermemory, failures_before_success: int, respx_mock: MockRouter - ) -> None: - client = client.with_options(max_retries=4) - - nb_retries = 0 - - def retry_handler(_request: httpx.Request) -> httpx.Response: - nonlocal nb_retries - if nb_retries < failures_before_success: - nb_retries += 1 - return httpx.Response(500) - return httpx.Response(200) - - respx_mock.post("/v3/documents").mock(side_effect=retry_handler) - - response = client.with_raw_response.add(content="content", extra_headers={"x-stainless-retry-count": Omit()}) - - assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 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) - def test_overwrite_retry_count_header( - self, client: Supermemory, failures_before_success: int, respx_mock: MockRouter - ) -> None: - client = client.with_options(max_retries=4) - - nb_retries = 0 - def retry_handler(_request: httpx.Request) -> httpx.Response: - nonlocal nb_retries - if nb_retries < failures_before_success: - nb_retries += 1 - return httpx.Response(500) - return httpx.Response(200) - respx_mock.post("/v3/documents").mock(side_effect=retry_handler) - - response = client.with_raw_response.add(content="content", extra_headers={"x-stainless-retry-count": "42"}) - - assert response.http_request.headers.get("x-stainless-retry-count") == "42" - - def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: - # Test that the proxy environment variables are set correctly - monkeypatch.setenv("HTTPS_PROXY", "https://example.org") - # Delete in case our environment has any proxy env vars set - monkeypatch.delenv("HTTP_PROXY", raising=False) - monkeypatch.delenv("ALL_PROXY", raising=False) - monkeypatch.delenv("NO_PROXY", raising=False) - monkeypatch.delenv("http_proxy", raising=False) - monkeypatch.delenv("https_proxy", raising=False) - monkeypatch.delenv("all_proxy", raising=False) - monkeypatch.delenv("no_proxy", raising=False) - - client = DefaultHttpxClient() - - mounts = tuple(client._mounts.items()) - assert len(mounts) == 1 - assert mounts[0][0].pattern == "https://" - - @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") - def test_default_client_creation(self) -> None: - # Ensure that the client can be initialized without any exceptions - DefaultHttpxClient( - verify=True, - cert=None, - trust_env=True, - http1=True, - http2=False, - limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), - ) - - @pytest.mark.respx(base_url=base_url) - 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 = 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, 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: - 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: - @pytest.mark.respx(base_url=base_url) - 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 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) - 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 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, async_client: AsyncSupermemory) -> None: - copied = async_client.copy() - assert id(copied) != id(async_client) - - copied = async_client.copy(api_key="another My API Key") - assert copied.api_key == "another My API Key" - assert async_client.api_key == "My API Key" - - def test_copy_default_options(self, async_client: AsyncSupermemory) -> None: - # options that have a default are overridden correctly - copied = async_client.copy(max_retries=7) - assert copied.max_retries == 7 - 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(async_client.timeout, httpx.Timeout) - copied = async_client.copy(timeout=None) - assert copied.timeout is None - assert isinstance(async_client.timeout, httpx.Timeout) - - 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"} - ) - assert client.default_headers["X-Foo"] == "bar" - - # does not override the already given value when not specified - copied = client.copy() - assert copied.default_headers["X-Foo"] == "bar" - - # merges already given headers - copied = client.copy(default_headers={"X-Bar": "stainless"}) - assert copied.default_headers["X-Foo"] == "bar" - assert copied.default_headers["X-Bar"] == "stainless" - - # uses new values for any already given headers - copied = client.copy(default_headers={"X-Foo": "stainless"}) - assert copied.default_headers["X-Foo"] == "stainless" - - # set_default_headers - - # completely overrides already set values - copied = client.copy(set_default_headers={}) - assert copied.default_headers.get("X-Foo") is None - - copied = client.copy(set_default_headers={"X-Bar": "Robert"}) - assert copied.default_headers["X-Bar"] == "Robert" - - with pytest.raises( - ValueError, - match="`default_headers` and `set_default_headers` arguments are mutually exclusive", - ): - client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) - await client.close() - - 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"} - ) - assert _get_params(client)["foo"] == "bar" - - # does not override the already given value when not specified - copied = client.copy() - assert _get_params(copied)["foo"] == "bar" - - # merges already given params - copied = client.copy(default_query={"bar": "stainless"}) - params = _get_params(copied) - assert params["foo"] == "bar" - assert params["bar"] == "stainless" - - # uses new values for any already given headers - copied = client.copy(default_query={"foo": "stainless"}) - assert _get_params(copied)["foo"] == "stainless" - - # set_default_query - - # completely overrides already set values - copied = client.copy(set_default_query={}) - assert _get_params(copied) == {} - - copied = client.copy(set_default_query={"bar": "Robert"}) - assert _get_params(copied)["bar"] == "Robert" - - with pytest.raises( - ValueError, - # TODO: update - match="`default_query` and `set_default_query` arguments are mutually exclusive", - ): - client.copy(set_default_query={}, default_query={"foo": "Bar"}) - - 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. - async_client.__init__, # type: ignore[misc] - ) - copy_signature = inspect.signature(async_client.copy) - exclude_params = {"transport", "proxies", "_strict_response_validation"} - - for name in init_signature.parameters.keys(): - if name in exclude_params: - continue - - copy_param = copy_signature.parameters.get(name) - assert copy_param is not None, f"copy() signature is missing the {name} param" - - @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") - def test_copy_build_request(self, async_client: AsyncSupermemory) -> None: - options = FinalRequestOptions(method="get", url="/foo") - - def build_request(options: FinalRequestOptions) -> None: - client_copy = async_client.copy() - client_copy._build_request(options) - - # ensure that the machinery is warmed up before tracing starts. - build_request(options) - gc.collect() - - tracemalloc.start(1000) - - snapshot_before = tracemalloc.take_snapshot() - - ITERATIONS = 10 - for _ in range(ITERATIONS): - build_request(options) - - gc.collect() - snapshot_after = tracemalloc.take_snapshot() - - tracemalloc.stop() - - def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None: - if diff.count == 0: - # Avoid false positives by considering only leaks (i.e. allocations that persist). - return - - if diff.count % ITERATIONS != 0: - # Avoid false positives by considering only leaks that appear per iteration. - return - - for frame in diff.traceback: - if any( - frame.filename.endswith(fragment) - for fragment in [ - # to_raw_response_wrapper leaks through the @functools.wraps() decorator. - # - # removing the decorator fixes the leak for reasons we don't understand. - "supermemory/_legacy_response.py", - "supermemory/_response.py", - # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. - "supermemory/_compat.py", - # Standard library leaks we don't care about. - "/logging/__init__.py", - ] - ): - return - - leaks.append(diff) - - leaks: list[tracemalloc.StatisticDiff] = [] - for diff in snapshot_after.compare_to(snapshot_before, "traceback"): - add_leak(leaks, diff) - if leaks: - for leak in leaks: - print("MEMORY LEAK:", leak) - for frame in leak.traceback: - print(frame) - raise AssertionError() - - 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 = async_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) - - async def test_client_timeout_option(self) -> None: - client = AsyncSupermemory( - base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) - ) - - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - 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: - client = AsyncSupermemory( - base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client - ) - - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - 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( - base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client - ) - - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - 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( - base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client - ) - - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - 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: - AsyncSupermemory( - base_url=base_url, - api_key=api_key, - _strict_response_validation=True, - http_client=cast(Any, http_client), - ) - - 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 = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) - assert request.headers.get("x-foo") == "bar" - assert request.headers.get("x-stainless-lang") == "python" - - test_client2 = AsyncSupermemory( - base_url=base_url, - api_key=api_key, - _strict_response_validation=True, - default_headers={ - "X-Foo": "stainless", - "X-Stainless-Lang": "my-overriding-header", - }, - ) - 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")) - assert request.headers.get("Authorization") == f"Bearer {api_key}" - - with pytest.raises(SupermemoryError): - with update_env(**{"SUPERMEMORY_API_KEY": Omit()}): - client2 = AsyncSupermemory(base_url=base_url, api_key=None, _strict_response_validation=True) - _ = client2 - - 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"} - ) - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) - url = httpx.URL(request.url) - assert dict(url.params) == {"query_param": "bar"} - - request = client._build_request( - FinalRequestOptions( - method="get", - url="/foo", - params={"foo": "baz", "query_param": "overridden"}, - ) - ) - url = httpx.URL(request.url) - assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} - - await client.close() - - async def test_hardcoded_query_params_in_url(self, async_client: AsyncSupermemory) -> None: - request = async_client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) - url = httpx.URL(request.url) - assert dict(url.params) == {"beta": "true"} - - request = async_client._build_request( - FinalRequestOptions( - method="get", - url="/foo?beta=true", - params={"limit": "10", "page": "abc"}, - ) - ) - url = httpx.URL(request.url) - assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} - - request = async_client._build_request( - FinalRequestOptions( - method="get", - url="/files/a%2Fb?beta=true", - params={"limit": "10"}, - ) - ) - assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" - - def test_request_extra_json(self, client: Supermemory) -> None: - request = client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - json_data={"foo": "bar"}, - extra_json={"baz": False}, - ), - ) - data = json.loads(request.content.decode("utf-8")) - assert data == {"foo": "bar", "baz": False} - - request = client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - extra_json={"baz": False}, - ), - ) - data = json.loads(request.content.decode("utf-8")) - assert data == {"baz": False} - - # `extra_json` takes priority over `json_data` when keys clash - request = client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - json_data={"foo": "bar", "baz": True}, - extra_json={"baz": None}, - ), - ) - data = json.loads(request.content.decode("utf-8")) - assert data == {"foo": "bar", "baz": None} - - def test_request_extra_headers(self, client: Supermemory) -> None: - request = client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - **make_request_options(extra_headers={"X-Foo": "Foo"}), - ), - ) - assert request.headers.get("X-Foo") == "Foo" - - # `extra_headers` takes priority over `default_headers` when keys clash - request = client.with_options(default_headers={"X-Bar": "true"})._build_request( - FinalRequestOptions( - method="post", - url="/foo", - **make_request_options( - extra_headers={"X-Bar": "false"}, - ), - ), - ) - assert request.headers.get("X-Bar") == "false" - - def test_request_extra_query(self, client: Supermemory) -> None: - request = client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - **make_request_options( - extra_query={"my_query_param": "Foo"}, - ), - ), - ) - params = dict(request.url.params) - assert params == {"my_query_param": "Foo"} - - # if both `query` and `extra_query` are given, they are merged - request = client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - **make_request_options( - query={"bar": "1"}, - extra_query={"foo": "2"}, - ), - ), - ) - params = dict(request.url.params) - assert params == {"bar": "1", "foo": "2"} - - # `extra_query` takes priority over `query` when keys clash - request = client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - **make_request_options( - query={"foo": "1"}, - extra_query={"foo": "2"}, - ), - ), - ) - params = dict(request.url.params) - assert params == {"foo": "2"} - - def test_multipart_repeating_array(self, async_client: AsyncSupermemory) -> None: - request = async_client._build_request( - FinalRequestOptions.construct( - method="post", - url="/foo", - headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, - json_data={"array": ["foo", "bar"]}, - files=[("foo.txt", b"hello world")], - ) - ) - - assert request.read().split(b"\r\n") == [ - b"--6b7ba517decee4a450543ea6ae821c82", - b'Content-Disposition: form-data; name="array[]"', - b"", - b"foo", - b"--6b7ba517decee4a450543ea6ae821c82", - b'Content-Disposition: form-data; name="array[]"', - b"", - b"bar", - b"--6b7ba517decee4a450543ea6ae821c82", - b'Content-Disposition: form-data; name="foo.txt"; filename="upload"', - b"Content-Type: application/octet-stream", - b"", - b"hello world", - b"--6b7ba517decee4a450543ea6ae821c82--", - b"", - ] - - @pytest.mark.respx(base_url=base_url) - async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncSupermemory) -> None: - respx_mock.post("/upload").mock(side_effect=mirror_request_content) - - file_content = b"Hello, this is a test file." - - response = await async_client.post( - "/upload", - content=file_content, - cast_to=httpx.Response, - options={"headers": {"Content-Type": "application/octet-stream"}}, - ) - - assert response.status_code == 200 - assert response.request.headers["Content-Type"] == "application/octet-stream" - assert response.content == file_content - - async def test_binary_content_upload_with_asynciterator(self) -> None: - file_content = b"Hello, this is a test file." - counter = Counter() - iterator = _make_async_iterator([file_content], counter=counter) - - async def mock_handler(request: httpx.Request) -> httpx.Response: - assert counter.value == 0, "the request body should not have been read" - return httpx.Response(200, content=await request.aread()) - - async with AsyncSupermemory( - base_url=base_url, - api_key=api_key, - _strict_response_validation=True, - http_client=httpx.AsyncClient(transport=MockTransport(handler=mock_handler)), - ) as client: - response = await client.post( - "/upload", - content=iterator, - cast_to=httpx.Response, - options={"headers": {"Content-Type": "application/octet-stream"}}, - ) - - assert response.status_code == 200 - assert response.request.headers["Content-Type"] == "application/octet-stream" - assert response.content == file_content - assert counter.value == 1 - - @pytest.mark.respx(base_url=base_url) - async def test_binary_content_upload_with_body_is_deprecated( - self, respx_mock: MockRouter, async_client: AsyncSupermemory - ) -> None: - respx_mock.post("/upload").mock(side_effect=mirror_request_content) - - file_content = b"Hello, this is a test file." - - with pytest.deprecated_call( - match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." - ): - response = await async_client.post( - "/upload", - body=file_content, - cast_to=httpx.Response, - options={"headers": {"Content-Type": "application/octet-stream"}}, - ) - - assert response.status_code == 200 - assert response.request.headers["Content-Type"] == "application/octet-stream" - assert response.content == file_content - - @pytest.mark.respx(base_url=base_url) - async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncSupermemory) -> None: - class Model1(BaseModel): - name: str - - class Model2(BaseModel): - foo: str - - respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - - 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, async_client: AsyncSupermemory) -> None: - """Union of objects with the same field name using a different type""" - - class Model1(BaseModel): - foo: int - - class Model2(BaseModel): - foo: str - - respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - - 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 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, async_client: AsyncSupermemory - ) -> None: - """ - Response that sets Content-Type to something other than application/json but returns json data - """ - - class Model(BaseModel): - foo: int - - respx_mock.get("/foo").mock( - return_value=httpx.Response( - 200, - content=json.dumps({"foo": 2}), - headers={"Content-Type": "application/text"}, - ) - ) - - response = await async_client.get("/foo", cast_to=Model) - assert isinstance(response, Model) - assert response.foo == 2 - - 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 - ) - assert client.base_url == "https://example.com/from_init/" - - client.base_url = "https://example.com/from_setter" # type: ignore[assignment] - - assert client.base_url == "https://example.com/from_setter/" - - 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/" - - @pytest.mark.parametrize( - "client", - [ - AsyncSupermemory( - base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True - ), - AsyncSupermemory( - base_url="http://localhost:5000/custom/path/", - api_key=api_key, - _strict_response_validation=True, - http_client=httpx.AsyncClient(), - ), - ], - ids=["standard", "custom http client"], +def _body(request: httpx.Request) -> dict[str, Any]: + return json.loads(request.content) if request.content else {} + + +CASES = [ + ("add", lambda c: c.add("user_alex", content="hi", id="pref-1"), "POST", "/ns/user_alex/document"), + ("search", lambda c: c.search("user_alex", query="hi"), "POST", "/ns/user_alex/search"), + ("profile", lambda c: c.profile("user_alex"), "POST", "/ns/user_alex/profile"), + ("list", lambda c: c.list("user_alex", "memories"), "POST", "/ns/user_alex/list/memories"), + ("documents.get", lambda c: c.documents.get("user_alex", "doc1"), "GET", "/ns/user_alex/document/doc1"), + ("documents.delete", lambda c: c.documents.delete("user_alex", ids=["d"]), "DELETE", "/ns/user_alex/document"), + ( + "documents.batch_add", + lambda c: c.documents.batch_add("user_alex", documents=[{"content": "x"}]), + "POST", + "/ns/user_alex/document/batch", + ), + ("memories.forget", lambda c: c.memories.forget("user_alex", ids=["m"]), "DELETE", "/ns/user_alex/memories"), + ("profiles.get_buckets", lambda c: c.profiles.get_buckets("user_alex"), "GET", "/ns/user_alex/profile/buckets"), + ("connectors.list", lambda c: c.connectors.list("user_alex"), "GET", "/ns/user_alex/connectors"), + ("namespaces.list", lambda c: c.namespaces.list(), "GET", "/ns"), + ("organization.get", lambda c: c.organization.get(), "GET", "/organization"), +] + + +@pytest.mark.parametrize("name,call,method,path", CASES, ids=[c[0] for c in CASES]) +def test_routes(name: str, call: Callable[[Supermemory], Any], method: str, path: str) -> None: + captured: Captured = [] + call(_client(captured)) + (request,) = captured + assert request.method == method, name + assert request.url.host == "api.supermemory.ai" + assert request.url.path == path + assert request.headers["authorization"] == "Bearer sm_test" + + +def test_body_uses_api_field_names() -> None: + captured: Captured = [] + _client(captured).search("user_alex", query="meetings", limit=5, rewrite_query=True) + (request,) = captured + assert request.url.params["limit"] == "5" + assert _body(request) == {"query": "meetings", "rewriteQuery": True} + + +def test_nested_dict_params_use_api_field_names() -> None: + captured: Captured = [] + client = _client(captured) + client.search( + "user_alex", + query="q", + filter={ + "operator": "and", + "operands": [ + {"field": "source", "operator": "eq", "value": "slack", "case_sensitive": False}, + {"field": "priority", "operator": "gte", "value": 3}, + ], + }, ) - async def test_base_url_trailing_slash(self, client: AsyncSupermemory) -> None: - request = client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - json_data={"foo": "bar"}, - ), - ) - assert request.url == "http://localhost:5000/custom/path/foo" - await client.close() - - @pytest.mark.parametrize( - "client", - [ - AsyncSupermemory( - base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True - ), - AsyncSupermemory( - base_url="http://localhost:5000/custom/path/", - api_key=api_key, - _strict_response_validation=True, - http_client=httpx.AsyncClient(), - ), - ], - ids=["standard", "custom http client"], + client.connectors.create( + "user_alex", + request={ + "provider": "s3", + "config": {"bucket": "b", "region": "us-east-1", "access_key_id": "AK", "secret_access_key": "SK"}, + "document_limit": 10, + }, ) - async def test_base_url_no_trailing_slash(self, client: AsyncSupermemory) -> None: - request = client._build_request( - FinalRequestOptions( - method="post", - url="/foo", - json_data={"foo": "bar"}, - ), - ) - assert request.url == "http://localhost:5000/custom/path/foo" - await client.close() - - @pytest.mark.parametrize( - "client", - [ - AsyncSupermemory( - base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True - ), - AsyncSupermemory( - base_url="http://localhost:5000/custom/path/", - api_key=api_key, - _strict_response_validation=True, - http_client=httpx.AsyncClient(), - ), + search, create = captured + assert _body(search)["filter"] == { + "operator": "and", + "operands": [ + {"field": "source", "operator": "eq", "value": "slack", "caseSensitive": False}, + {"field": "priority", "operator": "gte", "value": 3}, ], - ids=["standard", "custom http client"], - ) - async def test_absolute_request_url(self, client: AsyncSupermemory) -> None: - request = client._build_request( - FinalRequestOptions( - method="post", - url="https://myapi.com/foo", - json_data={"foo": "bar"}, - ), - ) - assert request.url == "https://myapi.com/foo" - await client.close() - - async def test_copied_client_does_not_close_http(self) -> None: - test_client = AsyncSupermemory(base_url=base_url, api_key=api_key, _strict_response_validation=True) - assert not test_client.is_closed() - - copied = test_client.copy() - assert copied is not test_client + } + assert create.url.path == "/ns/user_alex/connectors" + assert _body(create) == { + "provider": "s3", + "config": {"bucket": "b", "region": "us-east-1", "accessKeyId": "AK", "secretAccessKey": "SK"}, + "documentLimit": 10, + } - del copied - await asyncio.sleep(0.2) - assert not test_client.is_closed() +def test_api_key_from_env(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("SUPERMEMORY_API_KEY", "sm_env") + import importlib - async def test_client_context_manager(self) -> None: - 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 test_client.is_closed() - assert test_client.is_closed() + import supermemory.client - @pytest.mark.respx(base_url=base_url) - 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 async_client.get("/foo", cast_to=Model) - - assert isinstance(exc.value.__cause__, ValidationError) - - async def test_client_max_retries_validation(self) -> None: - with pytest.raises(TypeError, match=r"max_retries cannot be None"): - AsyncSupermemory( - base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None) - ) - - @pytest.mark.respx(base_url=base_url) - async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: - class Model(BaseModel): - name: str - - respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) - - strict_client = AsyncSupermemory(base_url=base_url, api_key=api_key, _strict_response_validation=True) - - with pytest.raises(APIResponseValidationError): - await strict_client.get("/foo", cast_to=Model) - - non_strict_client = AsyncSupermemory(base_url=base_url, api_key=api_key, _strict_response_validation=False) - - 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", - [ - [3, "20", 20], - [3, "0", 0.5], - [3, "-10", 0.5], - [3, "60", 60], - [3, "61", 0.5], - [3, "Fri, 29 Sep 2023 16:26:57 GMT", 20], - [3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5], - [3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5], - [3, "Fri, 29 Sep 2023 16:27:37 GMT", 60], - [3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5], - [3, "99999999999999999999999999999999999", 0.5], - [3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5], - [3, "", 0.5], - [2, "", 0.5 * 2.0], - [1, "", 0.5 * 4.0], - [-1100, "", 8], # test large number potentially overflowing - ], + importlib.reload(supermemory.client) # the default is read at import time + captured: Captured = [] + client = supermemory.client.Supermemory( + httpx_client=httpx.Client(transport=httpx.MockTransport(_responder(captured))) ) - @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) - 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 = 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) - @pytest.mark.respx(base_url=base_url) - async def test_retrying_timeout_errors_doesnt_leak( - self, respx_mock: MockRouter, async_client: AsyncSupermemory - ) -> None: - respx_mock.post("/v3/documents").mock(side_effect=httpx.TimeoutException("Test timeout error")) - - with pytest.raises(APITimeoutError): - await async_client.with_streaming_response.add(content="content").__aenter__() - - 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) - async def test_retrying_status_errors_doesnt_leak( - self, respx_mock: MockRouter, async_client: AsyncSupermemory - ) -> None: - respx_mock.post("/v3/documents").mock(return_value=httpx.Response(500)) - - with pytest.raises(APIStatusError): - await async_client.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.parametrize("failure_mode", ["status", "exception"]) - async def test_retries_taken( - self, - async_client: AsyncSupermemory, - failures_before_success: int, - failure_mode: Literal["status", "exception"], - respx_mock: MockRouter, - ) -> None: - client = async_client.with_options(max_retries=4) - - nb_retries = 0 - - def retry_handler(_request: httpx.Request) -> httpx.Response: - nonlocal nb_retries - if nb_retries < failures_before_success: - nb_retries += 1 - if failure_mode == "exception": - raise RuntimeError("oops") - return httpx.Response(500) - return httpx.Response(200) - - respx_mock.post("/v3/documents").mock(side_effect=retry_handler) + client.namespaces.list() + assert captured[0].headers["authorization"] == "Bearer sm_env" - response = await client.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 +def test_typed_error() -> None: + captured: Captured = [] + client = _client(captured, status=404, body={"error": "not found"}) + with pytest.raises(NotFoundError) as exc: + client.documents.get("user_alex", "missing") + assert isinstance(exc.value, ApiError) + assert exc.value.status_code == 404 - @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) - async def test_omit_retry_count_header( - self, async_client: AsyncSupermemory, failures_before_success: int, respx_mock: MockRouter - ) -> None: - client = async_client.with_options(max_retries=4) - nb_retries = 0 - - def retry_handler(_request: httpx.Request) -> httpx.Response: - nonlocal nb_retries - if nb_retries < failures_before_success: - nb_retries += 1 - return httpx.Response(500) - return httpx.Response(200) - - respx_mock.post("/v3/documents").mock(side_effect=retry_handler) - - response = await client.with_raw_response.add( - content="content", extra_headers={"x-stainless-retry-count": Omit()} - ) - - assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 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) - async def test_overwrite_retry_count_header( - self, async_client: AsyncSupermemory, failures_before_success: int, respx_mock: MockRouter - ) -> None: - client = async_client.with_options(max_retries=4) - - nb_retries = 0 - - def retry_handler(_request: httpx.Request) -> httpx.Response: - nonlocal nb_retries - if nb_retries < failures_before_success: - nb_retries += 1 - return httpx.Response(500) - return httpx.Response(200) - - respx_mock.post("/v3/documents").mock(side_effect=retry_handler) - - response = await client.with_raw_response.add( - content="content", extra_headers={"x-stainless-retry-count": "42"} - ) - - assert response.http_request.headers.get("x-stainless-retry-count") == "42" - - async def test_get_platform(self) -> None: - platform = await asyncify(get_platform)() - assert isinstance(platform, (str, OtherPlatform)) - - async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: - # Test that the proxy environment variables are set correctly - monkeypatch.setenv("HTTPS_PROXY", "https://example.org") - # Delete in case our environment has any proxy env vars set - monkeypatch.delenv("HTTP_PROXY", raising=False) - monkeypatch.delenv("ALL_PROXY", raising=False) - monkeypatch.delenv("NO_PROXY", raising=False) - monkeypatch.delenv("http_proxy", raising=False) - monkeypatch.delenv("https_proxy", raising=False) - monkeypatch.delenv("all_proxy", raising=False) - monkeypatch.delenv("no_proxy", raising=False) - - client = DefaultAsyncHttpxClient() - - mounts = tuple(client._mounts.items()) - assert len(mounts) == 1 - assert mounts[0][0].pattern == "https://" - - @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") - async def test_default_client_creation(self) -> None: - # Ensure that the client can be initialized without any exceptions - DefaultAsyncHttpxClient( - verify=True, - cert=None, - trust_env=True, - http1=True, - http2=False, - limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), - ) - - @pytest.mark.respx(base_url=base_url) - 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 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, 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 async_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" +async def test_async_client() -> None: + captured: Captured = [] + client = AsyncSupermemory( + api_key="sm_test", + httpx_client=httpx.AsyncClient(transport=httpx.MockTransport(_responder(captured))), + ) + await client.add("user_alex", content="hi") + assert captured[0].url.path == "/ns/user_alex/document" + assert _body(captured[0]) == {"content": "hi"} diff --git a/tests/test_compat.py b/tests/test_compat.py new file mode 100644 index 00000000..e46bc490 --- /dev/null +++ b/tests/test_compat.py @@ -0,0 +1,193 @@ +"""The 3.x (Stainless) client conventions kept by custom/supermemory/_compat.py.""" + +import json +from collections.abc import Callable +from typing import Any + +import httpx +import pytest + +import supermemory +from supermemory import ( + APIConnectionError, + APIError, + APIStatusError, + APITimeoutError, + AsyncSupermemory, + AuthenticationError, + InternalServerError, + NotFoundError, + RateLimitError, + Supermemory, + SupermemoryError, +) +from supermemory.errors import NotFoundError as GeneratedNotFoundError + +Handler = Callable[[httpx.Request], httpx.Response] + + +def _ok(captured: list[httpx.Request], body: Any = None) -> Handler: + def handle(request: httpx.Request) -> httpx.Response: + captured.append(request) + return httpx.Response(200, json={} if body is None else body) + + return handle + + +def _client(handler: Handler, **kwargs: Any) -> Supermemory: + return Supermemory(api_key="sm_test", http_client=httpx.Client(transport=httpx.MockTransport(handler)), **kwargs) + + +def test_missing_api_key(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("SUPERMEMORY_API_KEY", raising=False) + with pytest.raises(SupermemoryError, match="api_key client option must be set"): + Supermemory() + + +def test_env_base_url_and_custom_headers(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("SUPERMEMORY_BASE_URL", "https://staging.example.com") + monkeypatch.setenv("SUPERMEMORY_CUSTOM_HEADERS", "X-Team: core\nX-Env: test") + captured: list[httpx.Request] = [] + _client(_ok(captured)).namespaces.list() + assert str(captured[0].url) == "https://staging.example.com/ns" + assert captured[0].headers["x-team"] == "core" + assert captured[0].headers["x-env"] == "test" + + +def test_default_headers_and_query() -> None: + captured: list[httpx.Request] = [] + client = _client(_ok(captured), default_headers={"X-App": "demo"}, default_query={"trace": "1"}) + client.search("user_alex", query="hi") + assert captured[0].headers["x-app"] == "demo" + assert captured[0].url.params["trace"] == "1" + + +def test_per_call_options() -> None: + captured: list[httpx.Request] = [] + _client(_ok(captured)).search( + "user_alex", + query="hi", + extra_headers={"X-Req": "1"}, + extra_query={"debug": "true"}, + extra_body={"experimental": True}, + timeout=5, + ) + (request,) = captured + assert request.headers["x-req"] == "1" + assert request.url.params["debug"] == "true" + assert json.loads(request.content) == {"query": "hi", "experimental": True} + assert request.extensions["timeout"]["read"] == 5 + + +def test_with_options_keeps_settings() -> None: + captured: list[httpx.Request] = [] + client = _client(_ok(captured), default_headers={"X-App": "demo"}) + derived = client.with_options(max_retries=0, default_headers={"X-Extra": "1"}) + derived.namespaces.list() + assert derived.max_retries == 0 + assert derived.api_key == "sm_test" + assert captured[0].headers["x-app"] == "demo" + assert captured[0].headers["x-extra"] == "1" + + +@pytest.mark.parametrize( + "status,cls", + [ + (401, AuthenticationError), + (404, NotFoundError), + (429, RateLimitError), + (503, InternalServerError), + (418, APIStatusError), + ], +) +def test_status_errors(status: int, cls: type) -> None: + def handle(request: httpx.Request) -> httpx.Response: + return httpx.Response(status, json={"error": "nope"}, headers={"x-request-id": "req_1"}) + + client = _client(handle, max_retries=0) + with pytest.raises(cls) as exc: + client.documents.get("user_alex", "doc1") + err = exc.value + assert isinstance(err, APIStatusError) and isinstance(err, APIError) and isinstance(err, SupermemoryError) + assert err.status_code == status + assert err.body == {"error": "nope"} + assert err.message == f"Error code: {status} - {{'error': 'nope'}}" + assert str(err) == err.message + assert err.response is not None and err.response.headers["x-request-id"] == "req_1" + assert err.request is not None and err.request.url.path == "/ns/user_alex/document/doc1" + + +def test_status_error_also_matches_generated_class() -> None: + client = _client(lambda _: httpx.Response(404, json={"error": "missing"})) + with pytest.raises(GeneratedNotFoundError): + client.documents.get("user_alex", "doc1") + + +def test_timeout_error() -> None: + def handle(request: httpx.Request) -> httpx.Response: + raise httpx.ReadTimeout("slow", request=request) + + with pytest.raises(APITimeoutError) as exc: + _client(handle, max_retries=0).namespaces.list() + assert isinstance(exc.value, APIConnectionError) + assert exc.value.request is not None + + +def test_connection_error() -> None: + def handle(request: httpx.Request) -> httpx.Response: + raise httpx.ConnectError("refused", request=request) + + with pytest.raises(APIConnectionError) as exc: + _client(handle, max_retries=0).namespaces.list() + assert not isinstance(exc.value, APITimeoutError) + + +def test_raw_response() -> None: + captured: list[httpx.Request] = [] + client = _client(_ok(captured, {"results": [], "searchTime": 3.5})) + raw = client.with_raw_response.search("user_alex", query="hi", extra_headers={"X-Req": "1"}) + assert raw.status_code == 200 + assert raw.http_response.status_code == 200 + assert raw.parse().search_time == 3.5 + assert captured[0].headers["x-req"] == "1" + + +def test_model_helpers() -> None: + client = _client(_ok([], {"results": [], "searchTime": 3.5})) + response = client.search("user_alex", query="hi") + assert response.to_dict() == {"results": [], "searchTime": 3.5} + assert response.to_dict(use_api_names=False) == {"results": [], "search_time": 3.5} + assert json.loads(response.to_json()) == {"results": [], "searchTime": 3.5} + + +def test_exports() -> None: + assert supermemory.Client is Supermemory + assert supermemory.__version__ + assert supermemory.DEFAULT_MAX_RETRIES == 2 + assert supermemory.DEFAULT_TIMEOUT == httpx.Timeout(60, connect=5.0) + + +async def test_async_conventions() -> None: + captured: list[httpx.Request] = [] + + def handle(request: httpx.Request) -> httpx.Response: + captured.append(request) + if request.url.path.endswith("/missing"): + return httpx.Response(404, json={"error": "missing"}) + return httpx.Response(200, json={"results": [], "searchTime": 1}) + + client = AsyncSupermemory( + api_key="sm_test", + default_query={"trace": "1"}, + http_client=httpx.AsyncClient(transport=httpx.MockTransport(handle)), + ) + await client.search("user_alex", query="hi", extra_headers={"X-Req": "1"}) + assert captured[0].headers["x-req"] == "1" + assert captured[0].url.params["trace"] == "1" + + raw = await client.with_raw_response.search("user_alex", query="hi") + assert (await raw.parse()).search_time == 1 + + with pytest.raises(NotFoundError) as exc: + await client.documents.get("user_alex", "missing") + assert exc.value.response is not None and exc.value.response.status_code == 404 diff --git a/tests/test_docs.py b/tests/test_docs.py new file mode 100644 index 00000000..b77b1249 --- /dev/null +++ b/tests/test_docs.py @@ -0,0 +1,86 @@ +"""Run every ```python snippet in the docs against a mocked API, so docs can't rot. + +Snippets in one file share a namespace and run in order, like a reader would. +Snippets with top-level `await` run in an event loop. +""" + +import ast +import asyncio +import inspect +import re +from pathlib import Path +from typing import Any + +import httpx +import pytest + +ROOT = Path(__file__).resolve().parent.parent +DOCS = ["README.md", "MIGRATION.md"] + +_SEARCH = { + "results": [ + { + "id": "mem_1", + "memory": "Alex prefers morning meetings", + "similarity": 0.92, + "isLatest": True, + "metadata": {}, + "system": {"updatedAt": "2026-10-01T09:00:00Z"}, + } + ], + "searchTime": 12.5, +} +_RESPONSES: dict[str, Any] = { + "search": _SEARCH, + "profile": {"profile": {"static": [{"id": "p1", "memory": "Lives in SF"}], "dynamic": [], "buckets": {}}}, + "document": {"id": "doc_1", "status": "queued"}, +} + + +def _respond(request: httpx.Request) -> httpx.Response: + path = request.url.path + if "missing" in path: + return httpx.Response(404, json={"error": "Document not found"}, request=request) + for key, body in _RESPONSES.items(): + if path.rstrip("/").endswith(key) or f"/{key}/" in path: + return httpx.Response(200, json=body, request=request) + return httpx.Response(200, json={}, request=request) + + +class _Transport(httpx.BaseTransport): + def handle_request(self, request: httpx.Request) -> httpx.Response: + request.read() + return _respond(request) + + +class _AsyncTransport(httpx.AsyncBaseTransport): + async def handle_async_request(self, request: httpx.Request) -> httpx.Response: + await request.aread() + return _respond(request) + + +def _snippets(doc: str) -> list: + return re.findall(r"```python\n(.*?)```", (ROOT / doc).read_text(), re.S) + + +@pytest.mark.filterwarnings("error") +@pytest.mark.parametrize("doc", DOCS) +def test_doc_snippets_run(doc: str, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + monkeypatch.setenv("SUPERMEMORY_API_KEY", "sm_test") + monkeypatch.chdir(tmp_path) + (tmp_path / "notes.pdf").write_bytes(b"%PDF-1.4 test") + monkeypatch.setattr(httpx.HTTPTransport, "handle_request", _Transport.handle_request) + monkeypatch.setattr(httpx.AsyncHTTPTransport, "handle_async_request", _AsyncTransport.handle_async_request) + monkeypatch.setattr("builtins.print", lambda *_, **__: None) + + namespace: dict[str, Any] = {"__name__": "__docs__"} + snippets = _snippets(doc) + assert snippets, f"no python snippets in {doc}" + for index, snippet in enumerate(snippets, 1): + code = compile(snippet, f"{doc}#snippet{index}", "exec", flags=ast.PyCF_ALLOW_TOP_LEVEL_AWAIT) + try: + result = eval(code, namespace) + if inspect.iscoroutine(result): + asyncio.run(result) + except Exception as err: # noqa: BLE001 + raise AssertionError(f"{doc} snippet {index} failed:\n{snippet}") from err diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py deleted file mode 100644 index 418e8cb3..00000000 --- a/tests/test_extract_files.py +++ /dev/null @@ -1,91 +0,0 @@ -from __future__ import annotations - -from typing import Sequence - -import pytest - -from supermemory._types import FileTypes, ArrayFormat -from supermemory._utils import extract_files - - -def test_removes_files_from_input() -> None: - query = {"foo": "bar"} - assert extract_files(query, paths=[]) == [] - assert query == {"foo": "bar"} - - query2 = {"foo": b"Bar", "hello": "world"} - assert extract_files(query2, paths=[["foo"]]) == [("foo", b"Bar")] - assert query2 == {"hello": "world"} - - query3 = {"foo": {"foo": {"bar": b"Bar"}}, "hello": "world"} - assert extract_files(query3, paths=[["foo", "foo", "bar"]]) == [("foo[foo][bar]", b"Bar")] - assert query3 == {"foo": {"foo": {}}, "hello": "world"} - - query4 = {"foo": {"bar": b"Bar", "baz": "foo"}, "hello": "world"} - assert extract_files(query4, paths=[["foo", "bar"]]) == [("foo[bar]", b"Bar")] - assert query4 == {"hello": "world", "foo": {"baz": "foo"}} - - -def test_multiple_files() -> None: - query = {"documents": [{"file": b"My first file"}, {"file": b"My second file"}]} - assert extract_files(query, paths=[["documents", "", "file"]]) == [ - ("documents[][file]", b"My first file"), - ("documents[][file]", b"My second file"), - ] - assert query == {"documents": [{}, {}]} - - -def test_top_level_file_array() -> None: - query = {"files": [b"file one", b"file two"], "title": "hello"} - assert extract_files(query, paths=[["files", ""]]) == [("files[]", b"file one"), ("files[]", b"file two")] - assert query == {"title": "hello"} - - -@pytest.mark.parametrize( - "query,paths,expected", - [ - [ - {"foo": {"bar": "baz"}}, - [["foo", "", "bar"]], - [], - ], - [ - {"foo": ["bar", "baz"]}, - [["foo", "bar"]], - [], - ], - [ - {"foo": {"bar": "baz"}}, - [["foo", "foo"]], - [], - ], - ], - ids=["dict expecting array", "array expecting dict", "unknown keys"], -) -def test_ignores_incorrect_paths( - query: dict[str, object], - paths: Sequence[Sequence[str]], - expected: list[tuple[str, FileTypes]], -) -> None: - assert extract_files(query, paths=paths) == expected - - -@pytest.mark.parametrize( - "array_format,expected_top_level,expected_nested", - [ - ("brackets", [("files[]", b"a"), ("files[]", b"b")], [("items[][file]", b"a"), ("items[][file]", b"b")]), - ("repeat", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]), - ("comma", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]), - ("indices", [("files[0]", b"a"), ("files[1]", b"b")], [("items[0][file]", b"a"), ("items[1][file]", b"b")]), - ], -) -def test_array_format_controls_file_field_names( - array_format: ArrayFormat, - expected_top_level: list[tuple[str, FileTypes]], - expected_nested: list[tuple[str, FileTypes]], -) -> None: - top_level = {"files": [b"a", b"b"]} - assert extract_files(top_level, paths=[["files", ""]], array_format=array_format) == expected_top_level - - nested = {"items": [{"file": b"a"}, {"file": b"b"}]} - assert extract_files(nested, paths=[["items", "", "file"]], array_format=array_format) == expected_nested diff --git a/tests/test_files.py b/tests/test_files.py deleted file mode 100644 index e4e8dc7f..00000000 --- a/tests/test_files.py +++ /dev/null @@ -1,148 +0,0 @@ -from pathlib import Path - -import anyio -import pytest -from dirty_equals import IsDict, IsList, IsBytes, IsTuple - -from supermemory._files import to_httpx_files, deepcopy_with_paths, async_to_httpx_files -from supermemory._utils import extract_files - -readme_path = Path(__file__).parent.parent.joinpath("README.md") - - -def test_pathlib_includes_file_name() -> None: - result = to_httpx_files({"file": readme_path}) - print(result) - assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) - - -def test_tuple_input() -> None: - result = to_httpx_files([("file", readme_path)]) - print(result) - assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) - - -@pytest.mark.asyncio -async def test_async_pathlib_includes_file_name() -> None: - result = await async_to_httpx_files({"file": readme_path}) - print(result) - assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) - - -@pytest.mark.asyncio -async def test_async_supports_anyio_path() -> None: - result = await async_to_httpx_files({"file": anyio.Path(readme_path)}) - print(result) - assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) - - -@pytest.mark.asyncio -async def test_async_tuple_input() -> None: - result = await async_to_httpx_files([("file", readme_path)]) - print(result) - assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) - - -def test_string_not_allowed() -> None: - with pytest.raises(TypeError, match="Expected file types input to be a FileContent type or to be a tuple"): - to_httpx_files( - { - "file": "foo", # type: ignore - } - ) - - -def assert_different_identities(obj1: object, obj2: object) -> None: - assert obj1 == obj2 - assert obj1 is not obj2 - - -class TestDeepcopyWithPaths: - def test_copies_top_level_dict(self) -> None: - original = {"file": b"data", "other": "value"} - result = deepcopy_with_paths(original, [["file"]]) - assert_different_identities(result, original) - - def test_file_value_is_same_reference(self) -> None: - file_bytes = b"contents" - original = {"file": file_bytes} - result = deepcopy_with_paths(original, [["file"]]) - assert_different_identities(result, original) - assert result["file"] is file_bytes - - def test_list_popped_wholesale(self) -> None: - files = [b"f1", b"f2"] - original = {"files": files, "title": "t"} - result = deepcopy_with_paths(original, [["files", ""]]) - assert_different_identities(result, original) - result_files = result["files"] - assert isinstance(result_files, list) - assert_different_identities(result_files, files) - - def test_nested_array_path_copies_list_and_elements(self) -> None: - elem1 = {"file": b"f1", "extra": 1} - elem2 = {"file": b"f2", "extra": 2} - original = {"items": [elem1, elem2]} - result = deepcopy_with_paths(original, [["items", "", "file"]]) - assert_different_identities(result, original) - result_items = result["items"] - assert isinstance(result_items, list) - assert_different_identities(result_items, original["items"]) - assert_different_identities(result_items[0], elem1) - assert_different_identities(result_items[1], elem2) - - def test_empty_paths_returns_same_object(self) -> None: - original = {"foo": "bar"} - result = deepcopy_with_paths(original, []) - assert result is original - - def test_multiple_paths(self) -> None: - f1 = b"file1" - f2 = b"file2" - original = {"a": f1, "b": f2, "c": "unchanged"} - result = deepcopy_with_paths(original, [["a"], ["b"]]) - assert_different_identities(result, original) - assert result["a"] is f1 - assert result["b"] is f2 - assert result["c"] is original["c"] - - def test_extract_files_does_not_mutate_original_top_level(self) -> None: - file_bytes = b"contents" - original = {"file": file_bytes, "other": "value"} - - copied = deepcopy_with_paths(original, [["file"]]) - extracted = extract_files(copied, paths=[["file"]]) - - assert extracted == [("file", file_bytes)] - assert original == {"file": file_bytes, "other": "value"} - assert copied == {"other": "value"} - - def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None: - file1 = b"f1" - file2 = b"f2" - original = { - "items": [ - {"file": file1, "extra": 1}, - {"file": file2, "extra": 2}, - ], - "title": "example", - } - - copied = deepcopy_with_paths(original, [["items", "", "file"]]) - extracted = extract_files(copied, paths=[["items", "", "file"]]) - - assert [entry for _, entry in extracted] == [file1, file2] - assert original == { - "items": [ - {"file": file1, "extra": 1}, - {"file": file2, "extra": 2}, - ], - "title": "example", - } - assert copied == { - "items": [ - {"extra": 1}, - {"extra": 2}, - ], - "title": "example", - } diff --git a/tests/test_models.py b/tests/test_models.py deleted file mode 100644 index 745a5af8..00000000 --- a/tests/test_models.py +++ /dev/null @@ -1,1017 +0,0 @@ -import json -from typing import TYPE_CHECKING, Any, Dict, List, Union, Iterable, Optional, cast -from datetime import datetime, timezone -from collections import deque -from typing_extensions import Literal, Annotated, TypedDict, TypeAliasType - -import pytest -import pydantic -from pydantic import Field - -from supermemory._utils import PropertyInfo -from supermemory._compat import PYDANTIC_V1, parse_obj, model_dump, model_json -from supermemory._models import DISCRIMINATOR_CACHE, BaseModel, EagerIterable, construct_type - - -class BasicModel(BaseModel): - foo: str - - -@pytest.mark.parametrize("value", ["hello", 1], ids=["correct type", "mismatched"]) -def test_basic(value: object) -> None: - m = BasicModel.construct(foo=value) - assert m.foo == value - - -def test_directly_nested_model() -> None: - class NestedModel(BaseModel): - nested: BasicModel - - m = NestedModel.construct(nested={"foo": "Foo!"}) - assert m.nested.foo == "Foo!" - - # mismatched types - m = NestedModel.construct(nested="hello!") - assert cast(Any, m.nested) == "hello!" - - -def test_optional_nested_model() -> None: - class NestedModel(BaseModel): - nested: Optional[BasicModel] - - m1 = NestedModel.construct(nested=None) - assert m1.nested is None - - m2 = NestedModel.construct(nested={"foo": "bar"}) - assert m2.nested is not None - assert m2.nested.foo == "bar" - - # mismatched types - m3 = NestedModel.construct(nested={"foo"}) - assert isinstance(cast(Any, m3.nested), set) - assert cast(Any, m3.nested) == {"foo"} - - -def test_list_nested_model() -> None: - class NestedModel(BaseModel): - nested: List[BasicModel] - - m = NestedModel.construct(nested=[{"foo": "bar"}, {"foo": "2"}]) - assert m.nested is not None - assert isinstance(m.nested, list) - assert len(m.nested) == 2 - assert m.nested[0].foo == "bar" - assert m.nested[1].foo == "2" - - # mismatched types - m = NestedModel.construct(nested=True) - assert cast(Any, m.nested) is True - - m = NestedModel.construct(nested=[False]) - assert cast(Any, m.nested) == [False] - - -def test_optional_list_nested_model() -> None: - class NestedModel(BaseModel): - nested: Optional[List[BasicModel]] - - m1 = NestedModel.construct(nested=[{"foo": "bar"}, {"foo": "2"}]) - assert m1.nested is not None - assert isinstance(m1.nested, list) - assert len(m1.nested) == 2 - assert m1.nested[0].foo == "bar" - assert m1.nested[1].foo == "2" - - m2 = NestedModel.construct(nested=None) - assert m2.nested is None - - # mismatched types - m3 = NestedModel.construct(nested={1}) - assert cast(Any, m3.nested) == {1} - - m4 = NestedModel.construct(nested=[False]) - assert cast(Any, m4.nested) == [False] - - -def test_list_optional_items_nested_model() -> None: - class NestedModel(BaseModel): - nested: List[Optional[BasicModel]] - - m = NestedModel.construct(nested=[None, {"foo": "bar"}]) - assert m.nested is not None - assert isinstance(m.nested, list) - assert len(m.nested) == 2 - assert m.nested[0] is None - assert m.nested[1] is not None - assert m.nested[1].foo == "bar" - - # mismatched types - m3 = NestedModel.construct(nested="foo") - assert cast(Any, m3.nested) == "foo" - - m4 = NestedModel.construct(nested=[False]) - assert cast(Any, m4.nested) == [False] - - -def test_list_mismatched_type() -> None: - class NestedModel(BaseModel): - nested: List[str] - - m = NestedModel.construct(nested=False) - assert cast(Any, m.nested) is False - - -def test_raw_dictionary() -> None: - class NestedModel(BaseModel): - nested: Dict[str, str] - - m = NestedModel.construct(nested={"hello": "world"}) - assert m.nested == {"hello": "world"} - - # mismatched types - m = NestedModel.construct(nested=False) - assert cast(Any, m.nested) is False - - -def test_nested_dictionary_model() -> None: - class NestedModel(BaseModel): - nested: Dict[str, BasicModel] - - m = NestedModel.construct(nested={"hello": {"foo": "bar"}}) - assert isinstance(m.nested, dict) - assert m.nested["hello"].foo == "bar" - - # mismatched types - m = NestedModel.construct(nested={"hello": False}) - assert cast(Any, m.nested["hello"]) is False - - -def test_unknown_fields() -> None: - m1 = BasicModel.construct(foo="foo", unknown=1) - assert m1.foo == "foo" - assert cast(Any, m1).unknown == 1 - - m2 = BasicModel.construct(foo="foo", unknown={"foo_bar": True}) - assert m2.foo == "foo" - assert cast(Any, m2).unknown == {"foo_bar": True} - - assert model_dump(m2) == {"foo": "foo", "unknown": {"foo_bar": True}} - - -def test_strict_validation_unknown_fields() -> None: - class Model(BaseModel): - foo: str - - model = parse_obj(Model, dict(foo="hello!", user="Robert")) - assert model.foo == "hello!" - assert cast(Any, model).user == "Robert" - - assert model_dump(model) == {"foo": "hello!", "user": "Robert"} - - -def test_aliases() -> None: - class Model(BaseModel): - my_field: int = Field(alias="myField") - - m = Model.construct(myField=1) - assert m.my_field == 1 - - # mismatched types - m = Model.construct(myField={"hello": False}) - assert cast(Any, m.my_field) == {"hello": False} - - -def test_repr() -> None: - model = BasicModel(foo="bar") - assert str(model) == "BasicModel(foo='bar')" - assert repr(model) == "BasicModel(foo='bar')" - - -def test_repr_nested_model() -> None: - class Child(BaseModel): - name: str - age: int - - class Parent(BaseModel): - name: str - child: Child - - model = Parent(name="Robert", child=Child(name="Foo", age=5)) - assert str(model) == "Parent(name='Robert', child=Child(name='Foo', age=5))" - assert repr(model) == "Parent(name='Robert', child=Child(name='Foo', age=5))" - - -def test_optional_list() -> None: - class Submodel(BaseModel): - name: str - - class Model(BaseModel): - items: Optional[List[Submodel]] - - m = Model.construct(items=None) - assert m.items is None - - m = Model.construct(items=[]) - assert m.items == [] - - m = Model.construct(items=[{"name": "Robert"}]) - assert m.items is not None - assert len(m.items) == 1 - assert m.items[0].name == "Robert" - - -def test_nested_union_of_models() -> None: - class Submodel1(BaseModel): - bar: bool - - class Submodel2(BaseModel): - thing: str - - class Model(BaseModel): - foo: Union[Submodel1, Submodel2] - - m = Model.construct(foo={"thing": "hello"}) - assert isinstance(m.foo, Submodel2) - assert m.foo.thing == "hello" - - -def test_nested_union_of_mixed_types() -> None: - class Submodel1(BaseModel): - bar: bool - - class Model(BaseModel): - foo: Union[Submodel1, Literal[True], Literal["CARD_HOLDER"]] - - m = Model.construct(foo=True) - assert m.foo is True - - m = Model.construct(foo="CARD_HOLDER") - assert m.foo == "CARD_HOLDER" - - m = Model.construct(foo={"bar": False}) - assert isinstance(m.foo, Submodel1) - assert m.foo.bar is False - - -def test_nested_union_multiple_variants() -> None: - class Submodel1(BaseModel): - bar: bool - - class Submodel2(BaseModel): - thing: str - - class Submodel3(BaseModel): - foo: int - - class Model(BaseModel): - foo: Union[Submodel1, Submodel2, None, Submodel3] - - m = Model.construct(foo={"thing": "hello"}) - assert isinstance(m.foo, Submodel2) - assert m.foo.thing == "hello" - - m = Model.construct(foo=None) - assert m.foo is None - - m = Model.construct() - assert m.foo is None - - m = Model.construct(foo={"foo": "1"}) - assert isinstance(m.foo, Submodel3) - assert m.foo.foo == 1 - - -def test_nested_union_invalid_data() -> None: - class Submodel1(BaseModel): - level: int - - class Submodel2(BaseModel): - name: str - - class Model(BaseModel): - foo: Union[Submodel1, Submodel2] - - m = Model.construct(foo=True) - assert cast(bool, m.foo) is True - - m = Model.construct(foo={"name": 3}) - if PYDANTIC_V1: - assert isinstance(m.foo, Submodel2) - assert m.foo.name == "3" - else: - assert isinstance(m.foo, Submodel1) - assert m.foo.name == 3 # type: ignore - - -def test_list_of_unions() -> None: - class Submodel1(BaseModel): - level: int - - class Submodel2(BaseModel): - name: str - - class Model(BaseModel): - items: List[Union[Submodel1, Submodel2]] - - m = Model.construct(items=[{"level": 1}, {"name": "Robert"}]) - assert len(m.items) == 2 - assert isinstance(m.items[0], Submodel1) - assert m.items[0].level == 1 - assert isinstance(m.items[1], Submodel2) - assert m.items[1].name == "Robert" - - m = Model.construct(items=[{"level": -1}, 156]) - assert len(m.items) == 2 - assert isinstance(m.items[0], Submodel1) - assert m.items[0].level == -1 - assert cast(Any, m.items[1]) == 156 - - -def test_union_of_lists() -> None: - class SubModel1(BaseModel): - level: int - - class SubModel2(BaseModel): - name: str - - class Model(BaseModel): - items: Union[List[SubModel1], List[SubModel2]] - - # with one valid entry - m = Model.construct(items=[{"name": "Robert"}]) - assert len(m.items) == 1 - assert isinstance(m.items[0], SubModel2) - assert m.items[0].name == "Robert" - - # with two entries pointing to different types - m = Model.construct(items=[{"level": 1}, {"name": "Robert"}]) - assert len(m.items) == 2 - assert isinstance(m.items[0], SubModel1) - assert m.items[0].level == 1 - assert isinstance(m.items[1], SubModel1) - assert cast(Any, m.items[1]).name == "Robert" - - # with two entries pointing to *completely* different types - m = Model.construct(items=[{"level": -1}, 156]) - assert len(m.items) == 2 - assert isinstance(m.items[0], SubModel1) - assert m.items[0].level == -1 - assert cast(Any, m.items[1]) == 156 - - -def test_dict_of_union() -> None: - class SubModel1(BaseModel): - name: str - - class SubModel2(BaseModel): - foo: str - - class Model(BaseModel): - data: Dict[str, Union[SubModel1, SubModel2]] - - m = Model.construct(data={"hello": {"name": "there"}, "foo": {"foo": "bar"}}) - assert len(list(m.data.keys())) == 2 - assert isinstance(m.data["hello"], SubModel1) - assert m.data["hello"].name == "there" - assert isinstance(m.data["foo"], SubModel2) - assert m.data["foo"].foo == "bar" - - # TODO: test mismatched type - - -def test_double_nested_union() -> None: - class SubModel1(BaseModel): - name: str - - class SubModel2(BaseModel): - bar: str - - class Model(BaseModel): - data: Dict[str, List[Union[SubModel1, SubModel2]]] - - m = Model.construct(data={"foo": [{"bar": "baz"}, {"name": "Robert"}]}) - assert len(m.data["foo"]) == 2 - - entry1 = m.data["foo"][0] - assert isinstance(entry1, SubModel2) - assert entry1.bar == "baz" - - entry2 = m.data["foo"][1] - assert isinstance(entry2, SubModel1) - assert entry2.name == "Robert" - - # TODO: test mismatched type - - -def test_union_of_dict() -> None: - class SubModel1(BaseModel): - name: str - - class SubModel2(BaseModel): - foo: str - - class Model(BaseModel): - data: Union[Dict[str, SubModel1], Dict[str, SubModel2]] - - m = Model.construct(data={"hello": {"name": "there"}, "foo": {"foo": "bar"}}) - assert len(list(m.data.keys())) == 2 - assert isinstance(m.data["hello"], SubModel1) - assert m.data["hello"].name == "there" - assert isinstance(m.data["foo"], SubModel1) - assert cast(Any, m.data["foo"]).foo == "bar" - - -def test_iso8601_datetime() -> None: - class Model(BaseModel): - created_at: datetime - - expected = datetime(2019, 12, 27, 18, 11, 19, 117000, tzinfo=timezone.utc) - - if PYDANTIC_V1: - expected_json = '{"created_at": "2019-12-27T18:11:19.117000+00:00"}' - else: - expected_json = '{"created_at":"2019-12-27T18:11:19.117000Z"}' - - model = Model.construct(created_at="2019-12-27T18:11:19.117Z") - assert model.created_at == expected - assert model_json(model) == expected_json - - model = parse_obj(Model, dict(created_at="2019-12-27T18:11:19.117Z")) - assert model.created_at == expected - assert model_json(model) == expected_json - - -def test_does_not_coerce_int() -> None: - class Model(BaseModel): - bar: int - - assert Model.construct(bar=1).bar == 1 - assert Model.construct(bar=10.9).bar == 10.9 - assert Model.construct(bar="19").bar == "19" # type: ignore[comparison-overlap] - assert Model.construct(bar=False).bar is False - - -def test_int_to_float_safe_conversion() -> None: - class Model(BaseModel): - float_field: float - - m = Model.construct(float_field=10) - assert m.float_field == 10.0 - assert isinstance(m.float_field, float) - - m = Model.construct(float_field=10.12) - assert m.float_field == 10.12 - assert isinstance(m.float_field, float) - - # number too big - m = Model.construct(float_field=2**53 + 1) - assert m.float_field == 2**53 + 1 - assert isinstance(m.float_field, int) - - -def test_deprecated_alias() -> None: - class Model(BaseModel): - resource_id: str = Field(alias="model_id") - - @property - def model_id(self) -> str: - return self.resource_id - - m = Model.construct(model_id="id") - assert m.model_id == "id" - assert m.resource_id == "id" - assert m.resource_id is m.model_id - - m = parse_obj(Model, {"model_id": "id"}) - assert m.model_id == "id" - assert m.resource_id == "id" - assert m.resource_id is m.model_id - - -def test_omitted_fields() -> None: - class Model(BaseModel): - resource_id: Optional[str] = None - - m = Model.construct() - assert m.resource_id is None - assert "resource_id" not in m.model_fields_set - - m = Model.construct(resource_id=None) - assert m.resource_id is None - assert "resource_id" in m.model_fields_set - - m = Model.construct(resource_id="foo") - assert m.resource_id == "foo" - assert "resource_id" in m.model_fields_set - - -def test_to_dict() -> None: - class Model(BaseModel): - foo: Optional[str] = Field(alias="FOO", default=None) - - m = Model(FOO="hello") - assert m.to_dict() == {"FOO": "hello"} - assert m.to_dict(use_api_names=False) == {"foo": "hello"} - - m2 = Model() - assert m2.to_dict() == {} - assert m2.to_dict(exclude_unset=False) == {"FOO": None} - assert m2.to_dict(exclude_unset=False, exclude_none=True) == {} - assert m2.to_dict(exclude_unset=False, exclude_defaults=True) == {} - - m3 = Model(FOO=None) - assert m3.to_dict() == {"FOO": None} - assert m3.to_dict(exclude_none=True) == {} - assert m3.to_dict(exclude_defaults=True) == {} - - class Model2(BaseModel): - created_at: datetime - - time_str = "2024-03-21T11:39:01.275859" - m4 = Model2.construct(created_at=time_str) - assert m4.to_dict(mode="python") == {"created_at": datetime.fromisoformat(time_str)} - assert m4.to_dict(mode="json") == {"created_at": time_str} - - if PYDANTIC_V1: - with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): - m.to_dict(warnings=False) - - -def test_forwards_compat_model_dump_method() -> None: - class Model(BaseModel): - foo: Optional[str] = Field(alias="FOO", default=None) - - m = Model(FOO="hello") - assert m.model_dump() == {"foo": "hello"} - assert m.model_dump(include={"bar"}) == {} - assert m.model_dump(exclude={"foo"}) == {} - assert m.model_dump(by_alias=True) == {"FOO": "hello"} - - m2 = Model() - assert m2.model_dump() == {"foo": None} - assert m2.model_dump(exclude_unset=True) == {} - assert m2.model_dump(exclude_none=True) == {} - assert m2.model_dump(exclude_defaults=True) == {} - - m3 = Model(FOO=None) - assert m3.model_dump() == {"foo": None} - assert m3.model_dump(exclude_none=True) == {} - - if PYDANTIC_V1: - with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): - m.model_dump(round_trip=True) - - with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): - m.model_dump(warnings=False) - - -def test_compat_method_no_error_for_warnings() -> None: - class Model(BaseModel): - foo: Optional[str] - - m = Model(foo="hello") - assert isinstance(model_dump(m, warnings=False), dict) - - -def test_to_json() -> None: - class Model(BaseModel): - foo: Optional[str] = Field(alias="FOO", default=None) - - m = Model(FOO="hello") - assert json.loads(m.to_json()) == {"FOO": "hello"} - assert json.loads(m.to_json(use_api_names=False)) == {"foo": "hello"} - - if PYDANTIC_V1: - assert m.to_json(indent=None) == '{"FOO": "hello"}' - else: - assert m.to_json(indent=None) == '{"FOO":"hello"}' - - m2 = Model() - assert json.loads(m2.to_json()) == {} - assert json.loads(m2.to_json(exclude_unset=False)) == {"FOO": None} - assert json.loads(m2.to_json(exclude_unset=False, exclude_none=True)) == {} - assert json.loads(m2.to_json(exclude_unset=False, exclude_defaults=True)) == {} - - m3 = Model(FOO=None) - assert json.loads(m3.to_json()) == {"FOO": None} - assert json.loads(m3.to_json(exclude_none=True)) == {} - - if PYDANTIC_V1: - with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): - m.to_json(warnings=False) - - -def test_forwards_compat_model_dump_json_method() -> None: - class Model(BaseModel): - foo: Optional[str] = Field(alias="FOO", default=None) - - m = Model(FOO="hello") - assert json.loads(m.model_dump_json()) == {"foo": "hello"} - assert json.loads(m.model_dump_json(include={"bar"})) == {} - assert json.loads(m.model_dump_json(include={"foo"})) == {"foo": "hello"} - assert json.loads(m.model_dump_json(by_alias=True)) == {"FOO": "hello"} - - assert m.model_dump_json(indent=2) == '{\n "foo": "hello"\n}' - - m2 = Model() - assert json.loads(m2.model_dump_json()) == {"foo": None} - assert json.loads(m2.model_dump_json(exclude_unset=True)) == {} - assert json.loads(m2.model_dump_json(exclude_none=True)) == {} - assert json.loads(m2.model_dump_json(exclude_defaults=True)) == {} - - m3 = Model(FOO=None) - assert json.loads(m3.model_dump_json()) == {"foo": None} - assert json.loads(m3.model_dump_json(exclude_none=True)) == {} - - if PYDANTIC_V1: - with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): - m.model_dump_json(round_trip=True) - - with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): - m.model_dump_json(warnings=False) - - -def test_type_compat() -> None: - # our model type can be assigned to Pydantic's model type - - def takes_pydantic(model: pydantic.BaseModel) -> None: # noqa: ARG001 - ... - - class OurModel(BaseModel): - foo: Optional[str] = None - - takes_pydantic(OurModel()) - - -def test_annotated_types() -> None: - class Model(BaseModel): - value: str - - m = construct_type( - value={"value": "foo"}, - type_=cast(Any, Annotated[Model, "random metadata"]), - ) - assert isinstance(m, Model) - assert m.value == "foo" - - -def test_discriminated_unions_invalid_data() -> None: - class A(BaseModel): - type: Literal["a"] - - data: str - - class B(BaseModel): - type: Literal["b"] - - data: int - - m = construct_type( - value={"type": "b", "data": "foo"}, - type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), - ) - assert isinstance(m, B) - assert m.type == "b" - assert m.data == "foo" # type: ignore[comparison-overlap] - - m = construct_type( - value={"type": "a", "data": 100}, - type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), - ) - assert isinstance(m, A) - assert m.type == "a" - if PYDANTIC_V1: - # pydantic v1 automatically converts inputs to strings - # if the expected type is a str - assert m.data == "100" - else: - assert m.data == 100 # type: ignore[comparison-overlap] - - -def test_discriminated_unions_unknown_variant() -> None: - class A(BaseModel): - type: Literal["a"] - - data: str - - class B(BaseModel): - type: Literal["b"] - - data: int - - m = construct_type( - value={"type": "c", "data": None, "new_thing": "bar"}, - type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), - ) - - # just chooses the first variant - assert isinstance(m, A) - assert m.type == "c" # type: ignore[comparison-overlap] - assert m.data == None # type: ignore[unreachable] - assert m.new_thing == "bar" - - -def test_discriminated_unions_invalid_data_nested_unions() -> None: - class A(BaseModel): - type: Literal["a"] - - data: str - - class B(BaseModel): - type: Literal["b"] - - data: int - - class C(BaseModel): - type: Literal["c"] - - data: bool - - m = construct_type( - value={"type": "b", "data": "foo"}, - type_=cast(Any, Annotated[Union[Union[A, B], C], PropertyInfo(discriminator="type")]), - ) - assert isinstance(m, B) - assert m.type == "b" - assert m.data == "foo" # type: ignore[comparison-overlap] - - m = construct_type( - value={"type": "c", "data": "foo"}, - type_=cast(Any, Annotated[Union[Union[A, B], C], PropertyInfo(discriminator="type")]), - ) - assert isinstance(m, C) - assert m.type == "c" - assert m.data == "foo" # type: ignore[comparison-overlap] - - -def test_discriminated_unions_with_aliases_invalid_data() -> None: - class A(BaseModel): - foo_type: Literal["a"] = Field(alias="type") - - data: str - - class B(BaseModel): - foo_type: Literal["b"] = Field(alias="type") - - data: int - - m = construct_type( - value={"type": "b", "data": "foo"}, - type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="foo_type")]), - ) - assert isinstance(m, B) - assert m.foo_type == "b" - assert m.data == "foo" # type: ignore[comparison-overlap] - - m = construct_type( - value={"type": "a", "data": 100}, - type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="foo_type")]), - ) - assert isinstance(m, A) - assert m.foo_type == "a" - if PYDANTIC_V1: - # pydantic v1 automatically converts inputs to strings - # if the expected type is a str - assert m.data == "100" - else: - assert m.data == 100 # type: ignore[comparison-overlap] - - -def test_discriminated_unions_overlapping_discriminators_invalid_data() -> None: - class A(BaseModel): - type: Literal["a"] - - data: bool - - class B(BaseModel): - type: Literal["a"] - - data: int - - m = construct_type( - value={"type": "a", "data": "foo"}, - type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), - ) - assert isinstance(m, B) - assert m.type == "a" - assert m.data == "foo" # type: ignore[comparison-overlap] - - -def test_discriminated_unions_invalid_data_uses_cache() -> None: - class A(BaseModel): - type: Literal["a"] - - data: str - - class B(BaseModel): - type: Literal["b"] - - data: int - - UnionType = cast(Any, Union[A, B]) - - assert not DISCRIMINATOR_CACHE.get(UnionType) - - m = construct_type( - value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")]) - ) - assert isinstance(m, B) - assert m.type == "b" - assert m.data == "foo" # type: ignore[comparison-overlap] - - discriminator = DISCRIMINATOR_CACHE.get(UnionType) - assert discriminator is not None - - m = construct_type( - value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")]) - ) - assert isinstance(m, B) - assert m.type == "b" - assert m.data == "foo" # type: ignore[comparison-overlap] - - # if the discriminator details object stays the same between invocations then - # we hit the cache - assert DISCRIMINATOR_CACHE.get(UnionType) is discriminator - - -@pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1") -def test_type_alias_type() -> None: - Alias = TypeAliasType("Alias", str) # pyright: ignore - - class Model(BaseModel): - alias: Alias - union: Union[int, Alias] - - m = construct_type(value={"alias": "foo", "union": "bar"}, type_=Model) - assert isinstance(m, Model) - assert isinstance(m.alias, str) - assert m.alias == "foo" - assert isinstance(m.union, str) - assert m.union == "bar" - - -@pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1") -def test_field_named_cls() -> None: - class Model(BaseModel): - cls: str - - m = construct_type(value={"cls": "foo"}, type_=Model) - assert isinstance(m, Model) - assert isinstance(m.cls, str) - - -def test_discriminated_union_case() -> None: - class A(BaseModel): - type: Literal["a"] - - data: bool - - class B(BaseModel): - type: Literal["b"] - - data: List[Union[A, object]] - - class ModelA(BaseModel): - type: Literal["modelA"] - - data: int - - class ModelB(BaseModel): - type: Literal["modelB"] - - required: str - - data: Union[A, B] - - # when constructing ModelA | ModelB, value data doesn't match ModelB exactly - missing `required` - m = construct_type( - value={"type": "modelB", "data": {"type": "a", "data": True}}, - type_=cast(Any, Annotated[Union[ModelA, ModelB], PropertyInfo(discriminator="type")]), - ) - - assert isinstance(m, ModelB) - - -def test_nested_discriminated_union() -> None: - class InnerType1(BaseModel): - type: Literal["type_1"] - - class InnerModel(BaseModel): - inner_value: str - - class InnerType2(BaseModel): - type: Literal["type_2"] - some_inner_model: InnerModel - - class Type1(BaseModel): - base_type: Literal["base_type_1"] - value: Annotated[ - Union[ - InnerType1, - InnerType2, - ], - PropertyInfo(discriminator="type"), - ] - - class Type2(BaseModel): - base_type: Literal["base_type_2"] - - T = Annotated[ - Union[ - Type1, - Type2, - ], - PropertyInfo(discriminator="base_type"), - ] - - model = construct_type( - type_=T, - value={ - "base_type": "base_type_1", - "value": { - "type": "type_2", - }, - }, - ) - assert isinstance(model, Type1) - assert isinstance(model.value, InnerType2) - - -@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2 for now") -def test_extra_properties() -> None: - class Item(BaseModel): - prop: int - - class Model(BaseModel): - __pydantic_extra__: Dict[str, Item] = Field(init=False) # pyright: ignore[reportIncompatibleVariableOverride] - - other: str - - if TYPE_CHECKING: - - def __getattr__(self, attr: str) -> Item: ... - - model = construct_type( - type_=Model, - value={ - "a": {"prop": 1}, - "other": "foo", - }, - ) - assert isinstance(model, Model) - assert model.a.prop == 1 - assert isinstance(model.a, Item) - assert model.other == "foo" - - -# NOTE: Workaround for Pydantic Iterable behavior. -# Iterable fields are replaced with a ValidatorIterator and may be consumed -# during serialization, which can cause subsequent dumps to return empty data. -# See: https://github.com/pydantic/pydantic/issues/9541 -@pytest.mark.parametrize( - "data, expected_validated", - [ - ([1, 2, 3], [1, 2, 3]), - ((1, 2, 3), (1, 2, 3)), - (set([1, 2, 3]), set([1, 2, 3])), - (iter([1, 2, 3]), [1, 2, 3]), - ([], []), - ((x for x in [1, 2, 3]), [1, 2, 3]), - (map(lambda x: x, [1, 2, 3]), [1, 2, 3]), - (frozenset([1, 2, 3]), frozenset([1, 2, 3])), - (deque([1, 2, 3]), deque([1, 2, 3])), - ], - ids=["list", "tuple", "set", "iterator", "empty", "generator", "map", "frozenset", "deque"], -) -@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2") -def test_iterable_construction(data: Iterable[int], expected_validated: Iterable[int]) -> None: - class TypeWithIterable(TypedDict): - items: EagerIterable[int] - - class Model(BaseModel): - data: TypeWithIterable - - m = Model.model_validate({"data": {"items": data}}) - assert m.data["items"] == expected_validated - - # Verify repeated dumps don't lose data (the original bug) - assert m.model_dump()["data"]["items"] == list(expected_validated) - assert m.model_dump()["data"]["items"] == list(expected_validated) - - -@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2") -def test_iterable_construction_str_falls_back_to_list() -> None: - # str is iterable (over chars), but str(list_of_chars) produces the list's repr - # rather than reconstructing a string from items. We special-case str to fall - # back to list instead of attempting reconstruction. - class TypeWithIterable(TypedDict): - items: EagerIterable[str] - - class Model(BaseModel): - data: TypeWithIterable - - m = Model.model_validate({"data": {"items": "hello"}}) - - # falls back to list of chars rather than calling str(["h", "e", "l", "l", "o"]) - assert m.data["items"] == ["h", "e", "l", "l", "o"] - assert m.model_dump()["data"]["items"] == ["h", "e", "l", "l", "o"] diff --git a/tests/test_qs.py b/tests/test_qs.py deleted file mode 100644 index 8b0e27bb..00000000 --- a/tests/test_qs.py +++ /dev/null @@ -1,78 +0,0 @@ -from typing import Any, cast -from functools import partial -from urllib.parse import unquote - -import pytest - -from supermemory._qs import Querystring, stringify - - -def test_empty() -> None: - assert stringify({}) == "" - assert stringify({"a": {}}) == "" - assert stringify({"a": {"b": {"c": {}}}}) == "" - - -def test_basic() -> None: - assert stringify({"a": 1}) == "a=1" - assert stringify({"a": "b"}) == "a=b" - assert stringify({"a": True}) == "a=true" - assert stringify({"a": False}) == "a=false" - assert stringify({"a": 1.23456}) == "a=1.23456" - assert stringify({"a": None}) == "" - - -@pytest.mark.parametrize("method", ["class", "function"]) -def test_nested_dotted(method: str) -> None: - if method == "class": - serialise = Querystring(nested_format="dots").stringify - else: - serialise = partial(stringify, nested_format="dots") - - assert unquote(serialise({"a": {"b": "c"}})) == "a.b=c" - assert unquote(serialise({"a": {"b": "c", "d": "e", "f": "g"}})) == "a.b=c&a.d=e&a.f=g" - assert unquote(serialise({"a": {"b": {"c": {"d": "e"}}}})) == "a.b.c.d=e" - assert unquote(serialise({"a": {"b": True}})) == "a.b=true" - - -def test_nested_brackets() -> None: - assert unquote(stringify({"a": {"b": "c"}})) == "a[b]=c" - assert unquote(stringify({"a": {"b": "c", "d": "e", "f": "g"}})) == "a[b]=c&a[d]=e&a[f]=g" - assert unquote(stringify({"a": {"b": {"c": {"d": "e"}}}})) == "a[b][c][d]=e" - assert unquote(stringify({"a": {"b": True}})) == "a[b]=true" - - -@pytest.mark.parametrize("method", ["class", "function"]) -def test_array_comma(method: str) -> None: - if method == "class": - serialise = Querystring(array_format="comma").stringify - else: - serialise = partial(stringify, array_format="comma") - - assert unquote(serialise({"in": ["foo", "bar"]})) == "in=foo,bar" - assert unquote(serialise({"a": {"b": [True, False]}})) == "a[b]=true,false" - assert unquote(serialise({"a": {"b": [True, False, None, True]}})) == "a[b]=true,false,true" - - -def test_array_repeat() -> None: - assert unquote(stringify({"in": ["foo", "bar"]})) == "in=foo&in=bar" - assert unquote(stringify({"a": {"b": [True, False]}})) == "a[b]=true&a[b]=false" - assert unquote(stringify({"a": {"b": [True, False, None, True]}})) == "a[b]=true&a[b]=false&a[b]=true" - assert unquote(stringify({"in": ["foo", {"b": {"c": ["d", "e"]}}]})) == "in=foo&in[b][c]=d&in[b][c]=e" - - -@pytest.mark.parametrize("method", ["class", "function"]) -def test_array_brackets(method: str) -> None: - if method == "class": - serialise = Querystring(array_format="brackets").stringify - else: - serialise = partial(stringify, array_format="brackets") - - assert unquote(serialise({"in": ["foo", "bar"]})) == "in[]=foo&in[]=bar" - assert unquote(serialise({"a": {"b": [True, False]}})) == "a[b][]=true&a[b][]=false" - assert unquote(serialise({"a": {"b": [True, False, None, True]}})) == "a[b][]=true&a[b][]=false&a[b][]=true" - - -def test_unknown_array_format() -> None: - with pytest.raises(NotImplementedError, match="Unknown array_format value: foo, choose from comma, repeat"): - stringify({"a": ["foo", "bar"]}, array_format=cast(Any, "foo")) diff --git a/tests/test_required_args.py b/tests/test_required_args.py deleted file mode 100644 index d886edee..00000000 --- a/tests/test_required_args.py +++ /dev/null @@ -1,111 +0,0 @@ -from __future__ import annotations - -import pytest - -from supermemory._utils import required_args - - -def test_too_many_positional_params() -> None: - @required_args(["a"]) - def foo(a: str | None = None) -> str | None: - return a - - with pytest.raises(TypeError, match=r"foo\(\) takes 1 argument\(s\) but 2 were given"): - foo("a", "b") # type: ignore - - -def test_positional_param() -> None: - @required_args(["a"]) - def foo(a: str | None = None) -> str | None: - return a - - assert foo("a") == "a" - assert foo(None) is None - assert foo(a="b") == "b" - - with pytest.raises(TypeError, match="Missing required argument: 'a'"): - foo() - - -def test_keyword_only_param() -> None: - @required_args(["a"]) - def foo(*, a: str | None = None) -> str | None: - return a - - assert foo(a="a") == "a" - assert foo(a=None) is None - assert foo(a="b") == "b" - - with pytest.raises(TypeError, match="Missing required argument: 'a'"): - foo() - - -def test_multiple_params() -> None: - @required_args(["a", "b", "c"]) - def foo(a: str = "", *, b: str = "", c: str = "") -> str | None: - return f"{a} {b} {c}" - - assert foo(a="a", b="b", c="c") == "a b c" - - error_message = r"Missing required arguments.*" - - with pytest.raises(TypeError, match=error_message): - foo() - - with pytest.raises(TypeError, match=error_message): - foo(a="a") - - with pytest.raises(TypeError, match=error_message): - foo(b="b") - - with pytest.raises(TypeError, match=error_message): - foo(c="c") - - with pytest.raises(TypeError, match=r"Missing required argument: 'a'"): - foo(b="a", c="c") - - with pytest.raises(TypeError, match=r"Missing required argument: 'b'"): - foo("a", c="c") - - -def test_multiple_variants() -> None: - @required_args(["a"], ["b"]) - def foo(*, a: str | None = None, b: str | None = None) -> str | None: - return a if a is not None else b - - assert foo(a="foo") == "foo" - assert foo(b="bar") == "bar" - assert foo(a=None) is None - assert foo(b=None) is None - - # TODO: this error message could probably be improved - with pytest.raises( - TypeError, - match=r"Missing required arguments; Expected either \('a'\) or \('b'\) arguments to be given", - ): - foo() - - -def test_multiple_params_multiple_variants() -> None: - @required_args(["a", "b"], ["c"]) - def foo(*, a: str | None = None, b: str | None = None, c: str | None = None) -> str | None: - if a is not None: - return a - if b is not None: - return b - return c - - error_message = r"Missing required arguments; Expected either \('a' and 'b'\) or \('c'\) arguments to be given" - - with pytest.raises(TypeError, match=error_message): - foo(a="foo") - - with pytest.raises(TypeError, match=error_message): - foo(b="bar") - - with pytest.raises(TypeError, match=error_message): - foo() - - assert foo(a=None, b="bar") == "bar" - assert foo(c=None) is None - assert foo(c="foo") == "foo" diff --git a/tests/test_response.py b/tests/test_response.py deleted file mode 100644 index 829b3c8b..00000000 --- a/tests/test_response.py +++ /dev/null @@ -1,277 +0,0 @@ -import json -from typing import Any, List, Union, cast -from typing_extensions import Annotated - -import httpx -import pytest -import pydantic - -from supermemory import BaseModel, Supermemory, AsyncSupermemory -from supermemory._response import ( - APIResponse, - BaseAPIResponse, - AsyncAPIResponse, - BinaryAPIResponse, - AsyncBinaryAPIResponse, - extract_response_type, -) -from supermemory._streaming import Stream -from supermemory._base_client import FinalRequestOptions - - -class ConcreteBaseAPIResponse(APIResponse[bytes]): ... - - -class ConcreteAPIResponse(APIResponse[List[str]]): ... - - -class ConcreteAsyncAPIResponse(APIResponse[httpx.Response]): ... - - -def test_extract_response_type_direct_classes() -> None: - assert extract_response_type(BaseAPIResponse[str]) == str - assert extract_response_type(APIResponse[str]) == str - assert extract_response_type(AsyncAPIResponse[str]) == str - - -def test_extract_response_type_direct_class_missing_type_arg() -> None: - with pytest.raises( - RuntimeError, - match="Expected type to have a type argument at index 0 but it did not", - ): - extract_response_type(AsyncAPIResponse) - - -def test_extract_response_type_concrete_subclasses() -> None: - assert extract_response_type(ConcreteBaseAPIResponse) == bytes - assert extract_response_type(ConcreteAPIResponse) == List[str] - assert extract_response_type(ConcreteAsyncAPIResponse) == httpx.Response - - -def test_extract_response_type_binary_response() -> None: - assert extract_response_type(BinaryAPIResponse) == bytes - assert extract_response_type(AsyncBinaryAPIResponse) == bytes - - -class PydanticModel(pydantic.BaseModel): ... - - -def test_response_parse_mismatched_basemodel(client: Supermemory) -> None: - response = APIResponse( - raw=httpx.Response(200, content=b"foo"), - client=client, - stream=False, - stream_cls=None, - cast_to=str, - options=FinalRequestOptions.construct(method="get", url="/foo"), - ) - - with pytest.raises( - TypeError, - match="Pydantic models must subclass our base model type, e.g. `from supermemory import BaseModel`", - ): - response.parse(to=PydanticModel) - - -@pytest.mark.asyncio -async def test_async_response_parse_mismatched_basemodel(async_client: AsyncSupermemory) -> None: - response = AsyncAPIResponse( - raw=httpx.Response(200, content=b"foo"), - client=async_client, - stream=False, - stream_cls=None, - cast_to=str, - options=FinalRequestOptions.construct(method="get", url="/foo"), - ) - - with pytest.raises( - TypeError, - match="Pydantic models must subclass our base model type, e.g. `from supermemory import BaseModel`", - ): - await response.parse(to=PydanticModel) - - -def test_response_parse_custom_stream(client: Supermemory) -> None: - response = APIResponse( - raw=httpx.Response(200, content=b"foo"), - client=client, - stream=True, - stream_cls=None, - cast_to=str, - options=FinalRequestOptions.construct(method="get", url="/foo"), - ) - - stream = response.parse(to=Stream[int]) - assert stream._cast_to == int - - -@pytest.mark.asyncio -async def test_async_response_parse_custom_stream(async_client: AsyncSupermemory) -> None: - response = AsyncAPIResponse( - raw=httpx.Response(200, content=b"foo"), - client=async_client, - stream=True, - stream_cls=None, - cast_to=str, - options=FinalRequestOptions.construct(method="get", url="/foo"), - ) - - stream = await response.parse(to=Stream[int]) - assert stream._cast_to == int - - -class CustomModel(BaseModel): - foo: str - bar: int - - -def test_response_parse_custom_model(client: Supermemory) -> None: - response = APIResponse( - raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), - client=client, - stream=False, - stream_cls=None, - cast_to=str, - options=FinalRequestOptions.construct(method="get", url="/foo"), - ) - - obj = response.parse(to=CustomModel) - assert obj.foo == "hello!" - assert obj.bar == 2 - - -@pytest.mark.asyncio -async def test_async_response_parse_custom_model(async_client: AsyncSupermemory) -> None: - response = AsyncAPIResponse( - raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), - client=async_client, - stream=False, - stream_cls=None, - cast_to=str, - options=FinalRequestOptions.construct(method="get", url="/foo"), - ) - - obj = await response.parse(to=CustomModel) - assert obj.foo == "hello!" - assert obj.bar == 2 - - -def test_response_parse_annotated_type(client: Supermemory) -> None: - response = APIResponse( - raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), - client=client, - stream=False, - stream_cls=None, - cast_to=str, - options=FinalRequestOptions.construct(method="get", url="/foo"), - ) - - obj = response.parse( - to=cast("type[CustomModel]", Annotated[CustomModel, "random metadata"]), - ) - assert obj.foo == "hello!" - assert obj.bar == 2 - - -async def test_async_response_parse_annotated_type(async_client: AsyncSupermemory) -> None: - response = AsyncAPIResponse( - raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), - client=async_client, - stream=False, - stream_cls=None, - cast_to=str, - options=FinalRequestOptions.construct(method="get", url="/foo"), - ) - - obj = await response.parse( - to=cast("type[CustomModel]", Annotated[CustomModel, "random metadata"]), - ) - assert obj.foo == "hello!" - assert obj.bar == 2 - - -@pytest.mark.parametrize( - "content, expected", - [ - ("false", False), - ("true", True), - ("False", False), - ("True", True), - ("TrUe", True), - ("FalSe", False), - ], -) -def test_response_parse_bool(client: Supermemory, content: str, expected: bool) -> None: - response = APIResponse( - raw=httpx.Response(200, content=content), - client=client, - stream=False, - stream_cls=None, - cast_to=str, - options=FinalRequestOptions.construct(method="get", url="/foo"), - ) - - result = response.parse(to=bool) - assert result is expected - - -@pytest.mark.parametrize( - "content, expected", - [ - ("false", False), - ("true", True), - ("False", False), - ("True", True), - ("TrUe", True), - ("FalSe", False), - ], -) -async def test_async_response_parse_bool(client: AsyncSupermemory, content: str, expected: bool) -> None: - response = AsyncAPIResponse( - raw=httpx.Response(200, content=content), - client=client, - stream=False, - stream_cls=None, - cast_to=str, - options=FinalRequestOptions.construct(method="get", url="/foo"), - ) - - result = await response.parse(to=bool) - assert result is expected - - -class OtherModel(BaseModel): - a: str - - -@pytest.mark.parametrize("client", [False], indirect=True) # loose validation -def test_response_parse_expect_model_union_non_json_content(client: Supermemory) -> None: - response = APIResponse( - raw=httpx.Response(200, content=b"foo", headers={"Content-Type": "application/text"}), - client=client, - stream=False, - stream_cls=None, - cast_to=str, - options=FinalRequestOptions.construct(method="get", url="/foo"), - ) - - obj = response.parse(to=cast(Any, Union[CustomModel, OtherModel])) - assert isinstance(obj, str) - assert obj == "foo" - - -@pytest.mark.asyncio -@pytest.mark.parametrize("async_client", [False], indirect=True) # loose validation -async def test_async_response_parse_expect_model_union_non_json_content(async_client: AsyncSupermemory) -> None: - response = AsyncAPIResponse( - raw=httpx.Response(200, content=b"foo", headers={"Content-Type": "application/text"}), - client=async_client, - stream=False, - stream_cls=None, - cast_to=str, - options=FinalRequestOptions.construct(method="get", url="/foo"), - ) - - obj = await response.parse(to=cast(Any, Union[CustomModel, OtherModel])) - assert isinstance(obj, str) - assert obj == "foo" diff --git a/tests/test_streaming.py b/tests/test_streaming.py deleted file mode 100644 index ee2d2c5f..00000000 --- a/tests/test_streaming.py +++ /dev/null @@ -1,252 +0,0 @@ -from __future__ import annotations - -from typing import Iterator, AsyncIterator - -import httpx -import pytest - -from supermemory import Supermemory, AsyncSupermemory -from supermemory._streaming import Stream, AsyncStream, ServerSentEvent - - -@pytest.mark.asyncio -@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_basic(sync: bool, client: Supermemory, async_client: AsyncSupermemory) -> None: - def body() -> Iterator[bytes]: - yield b"event: completion\n" - yield b'data: {"foo":true}\n' - yield b"\n" - - iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) - - sse = await iter_next(iterator) - assert sse.event == "completion" - assert sse.json() == {"foo": True} - - await assert_empty_iter(iterator) - - -@pytest.mark.asyncio -@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_data_missing_event(sync: bool, client: Supermemory, async_client: AsyncSupermemory) -> None: - def body() -> Iterator[bytes]: - yield b'data: {"foo":true}\n' - yield b"\n" - - iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) - - sse = await iter_next(iterator) - assert sse.event is None - assert sse.json() == {"foo": True} - - await assert_empty_iter(iterator) - - -@pytest.mark.asyncio -@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_event_missing_data(sync: bool, client: Supermemory, async_client: AsyncSupermemory) -> None: - def body() -> Iterator[bytes]: - yield b"event: ping\n" - yield b"\n" - - iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) - - sse = await iter_next(iterator) - assert sse.event == "ping" - assert sse.data == "" - - await assert_empty_iter(iterator) - - -@pytest.mark.asyncio -@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_multiple_events(sync: bool, client: Supermemory, async_client: AsyncSupermemory) -> None: - def body() -> Iterator[bytes]: - yield b"event: ping\n" - yield b"\n" - yield b"event: completion\n" - yield b"\n" - - iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) - - sse = await iter_next(iterator) - assert sse.event == "ping" - assert sse.data == "" - - sse = await iter_next(iterator) - assert sse.event == "completion" - assert sse.data == "" - - await assert_empty_iter(iterator) - - -@pytest.mark.asyncio -@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_multiple_events_with_data(sync: bool, client: Supermemory, async_client: AsyncSupermemory) -> None: - def body() -> Iterator[bytes]: - yield b"event: ping\n" - yield b'data: {"foo":true}\n' - yield b"\n" - yield b"event: completion\n" - yield b'data: {"bar":false}\n' - yield b"\n" - - iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) - - sse = await iter_next(iterator) - assert sse.event == "ping" - assert sse.json() == {"foo": True} - - sse = await iter_next(iterator) - assert sse.event == "completion" - assert sse.json() == {"bar": False} - - await assert_empty_iter(iterator) - - -@pytest.mark.asyncio -@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_multiple_data_lines_with_empty_line( - sync: bool, client: Supermemory, async_client: AsyncSupermemory -) -> None: - def body() -> Iterator[bytes]: - yield b"event: ping\n" - yield b"data: {\n" - yield b'data: "foo":\n' - yield b"data: \n" - yield b"data:\n" - yield b"data: true}\n" - yield b"\n\n" - - iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) - - sse = await iter_next(iterator) - assert sse.event == "ping" - assert sse.json() == {"foo": True} - assert sse.data == '{\n"foo":\n\n\ntrue}' - - await assert_empty_iter(iterator) - - -@pytest.mark.asyncio -@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_data_json_escaped_double_new_line( - sync: bool, client: Supermemory, async_client: AsyncSupermemory -) -> None: - def body() -> Iterator[bytes]: - yield b"event: ping\n" - yield b'data: {"foo": "my long\\n\\ncontent"}' - yield b"\n\n" - - iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) - - sse = await iter_next(iterator) - assert sse.event == "ping" - assert sse.json() == {"foo": "my long\n\ncontent"} - - await assert_empty_iter(iterator) - - -@pytest.mark.asyncio -@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_multiple_data_lines(sync: bool, client: Supermemory, async_client: AsyncSupermemory) -> None: - def body() -> Iterator[bytes]: - yield b"event: ping\n" - yield b"data: {\n" - yield b'data: "foo":\n' - yield b"data: true}\n" - yield b"\n\n" - - iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) - - sse = await iter_next(iterator) - assert sse.event == "ping" - assert sse.json() == {"foo": True} - - await assert_empty_iter(iterator) - - -@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_special_new_line_character( - sync: bool, - client: Supermemory, - async_client: AsyncSupermemory, -) -> None: - def body() -> Iterator[bytes]: - yield b'data: {"content":" culpa"}\n' - yield b"\n" - yield b'data: {"content":" \xe2\x80\xa8"}\n' - yield b"\n" - yield b'data: {"content":"foo"}\n' - yield b"\n" - - iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) - - sse = await iter_next(iterator) - assert sse.event is None - assert sse.json() == {"content": " culpa"} - - sse = await iter_next(iterator) - assert sse.event is None - assert sse.json() == {"content": " 
"} - - sse = await iter_next(iterator) - assert sse.event is None - assert sse.json() == {"content": "foo"} - - await assert_empty_iter(iterator) - - -@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) -async def test_multi_byte_character_multiple_chunks( - sync: bool, - client: Supermemory, - async_client: AsyncSupermemory, -) -> None: - def body() -> Iterator[bytes]: - yield b'data: {"content":"' - # bytes taken from the string 'известни' and arbitrarily split - # so that some multi-byte characters span multiple chunks - yield b"\xd0" - yield b"\xb8\xd0\xb7\xd0" - yield b"\xb2\xd0\xb5\xd1\x81\xd1\x82\xd0\xbd\xd0\xb8" - yield b'"}\n' - yield b"\n" - - iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) - - sse = await iter_next(iterator) - assert sse.event is None - assert sse.json() == {"content": "известни"} - - -async def to_aiter(iter: Iterator[bytes]) -> AsyncIterator[bytes]: - for chunk in iter: - yield chunk - - -async def iter_next(iter: Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]) -> ServerSentEvent: - if isinstance(iter, AsyncIterator): - return await iter.__anext__() - - return next(iter) - - -async def assert_empty_iter(iter: Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]) -> None: - with pytest.raises((StopAsyncIteration, RuntimeError)): - await iter_next(iter) - - -def make_event_iterator( - content: Iterator[bytes], - *, - sync: bool, - client: Supermemory, - async_client: AsyncSupermemory, -) -> Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]: - if sync: - return Stream(cast_to=object, client=client, response=httpx.Response(200, content=content))._iter_events() - - return AsyncStream( - cast_to=object, client=async_client, response=httpx.Response(200, content=to_aiter(content)) - )._iter_events() diff --git a/tests/test_transform.py b/tests/test_transform.py deleted file mode 100644 index ab0bf040..00000000 --- a/tests/test_transform.py +++ /dev/null @@ -1,460 +0,0 @@ -from __future__ import annotations - -import io -import pathlib -from typing import Any, Dict, List, Union, TypeVar, Iterable, Optional, cast -from datetime import date, datetime -from typing_extensions import Required, Annotated, TypedDict - -import pytest - -from supermemory._types import Base64FileInput, omit, not_given -from supermemory._utils import ( - PropertyInfo, - transform as _transform, - parse_datetime, - async_transform as _async_transform, -) -from supermemory._compat import PYDANTIC_V1 -from supermemory._models import BaseModel - -_T = TypeVar("_T") - -SAMPLE_FILE_PATH = pathlib.Path(__file__).parent.joinpath("sample_file.txt") - - -async def transform( - data: _T, - expected_type: object, - use_async: bool, -) -> _T: - if use_async: - return await _async_transform(data, expected_type=expected_type) - - return _transform(data, expected_type=expected_type) - - -parametrize = pytest.mark.parametrize("use_async", [False, True], ids=["sync", "async"]) - - -class Foo1(TypedDict): - foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] - - -@parametrize -@pytest.mark.asyncio -async def test_top_level_alias(use_async: bool) -> None: - assert await transform({"foo_bar": "hello"}, expected_type=Foo1, use_async=use_async) == {"fooBar": "hello"} - - -class Foo2(TypedDict): - bar: Bar2 - - -class Bar2(TypedDict): - this_thing: Annotated[int, PropertyInfo(alias="this__thing")] - baz: Annotated[Baz2, PropertyInfo(alias="Baz")] - - -class Baz2(TypedDict): - my_baz: Annotated[str, PropertyInfo(alias="myBaz")] - - -@parametrize -@pytest.mark.asyncio -async def test_recursive_typeddict(use_async: bool) -> None: - assert await transform({"bar": {"this_thing": 1}}, Foo2, use_async) == {"bar": {"this__thing": 1}} - assert await transform({"bar": {"baz": {"my_baz": "foo"}}}, Foo2, use_async) == {"bar": {"Baz": {"myBaz": "foo"}}} - - -class Foo3(TypedDict): - things: List[Bar3] - - -class Bar3(TypedDict): - my_field: Annotated[str, PropertyInfo(alias="myField")] - - -@parametrize -@pytest.mark.asyncio -async def test_list_of_typeddict(use_async: bool) -> None: - result = await transform({"things": [{"my_field": "foo"}, {"my_field": "foo2"}]}, Foo3, use_async) - assert result == {"things": [{"myField": "foo"}, {"myField": "foo2"}]} - - -class Foo4(TypedDict): - foo: Union[Bar4, Baz4] - - -class Bar4(TypedDict): - foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] - - -class Baz4(TypedDict): - foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] - - -@parametrize -@pytest.mark.asyncio -async def test_union_of_typeddict(use_async: bool) -> None: - assert await transform({"foo": {"foo_bar": "bar"}}, Foo4, use_async) == {"foo": {"fooBar": "bar"}} - assert await transform({"foo": {"foo_baz": "baz"}}, Foo4, use_async) == {"foo": {"fooBaz": "baz"}} - assert await transform({"foo": {"foo_baz": "baz", "foo_bar": "bar"}}, Foo4, use_async) == { - "foo": {"fooBaz": "baz", "fooBar": "bar"} - } - - -class Foo5(TypedDict): - foo: Annotated[Union[Bar4, List[Baz4]], PropertyInfo(alias="FOO")] - - -class Bar5(TypedDict): - foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] - - -class Baz5(TypedDict): - foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] - - -@parametrize -@pytest.mark.asyncio -async def test_union_of_list(use_async: bool) -> None: - assert await transform({"foo": {"foo_bar": "bar"}}, Foo5, use_async) == {"FOO": {"fooBar": "bar"}} - assert await transform( - { - "foo": [ - {"foo_baz": "baz"}, - {"foo_baz": "baz"}, - ] - }, - Foo5, - use_async, - ) == {"FOO": [{"fooBaz": "baz"}, {"fooBaz": "baz"}]} - - -class Foo6(TypedDict): - bar: Annotated[str, PropertyInfo(alias="Bar")] - - -@parametrize -@pytest.mark.asyncio -async def test_includes_unknown_keys(use_async: bool) -> None: - assert await transform({"bar": "bar", "baz_": {"FOO": 1}}, Foo6, use_async) == { - "Bar": "bar", - "baz_": {"FOO": 1}, - } - - -class Foo7(TypedDict): - bar: Annotated[List[Bar7], PropertyInfo(alias="bAr")] - foo: Bar7 - - -class Bar7(TypedDict): - foo: str - - -@parametrize -@pytest.mark.asyncio -async def test_ignores_invalid_input(use_async: bool) -> None: - assert await transform({"bar": ""}, Foo7, use_async) == {"bAr": ""} - assert await transform({"foo": ""}, Foo7, use_async) == {"foo": ""} - - -class DatetimeDict(TypedDict, total=False): - foo: Annotated[datetime, PropertyInfo(format="iso8601")] - - bar: Annotated[Optional[datetime], PropertyInfo(format="iso8601")] - - required: Required[Annotated[Optional[datetime], PropertyInfo(format="iso8601")]] - - list_: Required[Annotated[Optional[List[datetime]], PropertyInfo(format="iso8601")]] - - union: Annotated[Union[int, datetime], PropertyInfo(format="iso8601")] - - -class DateDict(TypedDict, total=False): - foo: Annotated[date, PropertyInfo(format="iso8601")] - - -class DatetimeModel(BaseModel): - foo: datetime - - -class DateModel(BaseModel): - foo: Optional[date] - - -@parametrize -@pytest.mark.asyncio -async def test_iso8601_format(use_async: bool) -> None: - dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") - tz = "+00:00" if PYDANTIC_V1 else "Z" - assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap] - assert await transform(DatetimeModel(foo=dt), Any, use_async) == {"foo": "2023-02-23T14:16:36.337692" + tz} # type: ignore[comparison-overlap] - - dt = dt.replace(tzinfo=None) - assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692"} # type: ignore[comparison-overlap] - assert await transform(DatetimeModel(foo=dt), Any, use_async) == {"foo": "2023-02-23T14:16:36.337692"} # type: ignore[comparison-overlap] - - assert await transform({"foo": None}, DateDict, use_async) == {"foo": None} # type: ignore[comparison-overlap] - assert await transform(DateModel(foo=None), Any, use_async) == {"foo": None} # type: ignore - assert await transform({"foo": date.fromisoformat("2023-02-23")}, DateDict, use_async) == {"foo": "2023-02-23"} # type: ignore[comparison-overlap] - assert await transform(DateModel(foo=date.fromisoformat("2023-02-23")), DateDict, use_async) == { - "foo": "2023-02-23" - } # type: ignore[comparison-overlap] - - -@parametrize -@pytest.mark.asyncio -async def test_optional_iso8601_format(use_async: bool) -> None: - dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") - assert await transform({"bar": dt}, DatetimeDict, use_async) == {"bar": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap] - - assert await transform({"bar": None}, DatetimeDict, use_async) == {"bar": None} - - -@parametrize -@pytest.mark.asyncio -async def test_required_iso8601_format(use_async: bool) -> None: - dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") - assert await transform({"required": dt}, DatetimeDict, use_async) == { - "required": "2023-02-23T14:16:36.337692+00:00" - } # type: ignore[comparison-overlap] - - assert await transform({"required": None}, DatetimeDict, use_async) == {"required": None} - - -@parametrize -@pytest.mark.asyncio -async def test_union_datetime(use_async: bool) -> None: - dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") - assert await transform({"union": dt}, DatetimeDict, use_async) == { # type: ignore[comparison-overlap] - "union": "2023-02-23T14:16:36.337692+00:00" - } - - assert await transform({"union": "foo"}, DatetimeDict, use_async) == {"union": "foo"} - - -@parametrize -@pytest.mark.asyncio -async def test_nested_list_iso6801_format(use_async: bool) -> None: - dt1 = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") - dt2 = parse_datetime("2022-01-15T06:34:23Z") - assert await transform({"list_": [dt1, dt2]}, DatetimeDict, use_async) == { # type: ignore[comparison-overlap] - "list_": ["2023-02-23T14:16:36.337692+00:00", "2022-01-15T06:34:23+00:00"] - } - - -@parametrize -@pytest.mark.asyncio -async def test_datetime_custom_format(use_async: bool) -> None: - dt = parse_datetime("2022-01-15T06:34:23Z") - - result = await transform(dt, Annotated[datetime, PropertyInfo(format="custom", format_template="%H")], use_async) - assert result == "06" # type: ignore[comparison-overlap] - - -class DateDictWithRequiredAlias(TypedDict, total=False): - required_prop: Required[Annotated[date, PropertyInfo(format="iso8601", alias="prop")]] - - -@parametrize -@pytest.mark.asyncio -async def test_datetime_with_alias(use_async: bool) -> None: - assert await transform({"required_prop": None}, DateDictWithRequiredAlias, use_async) == {"prop": None} # type: ignore[comparison-overlap] - assert await transform( - {"required_prop": date.fromisoformat("2023-02-23")}, DateDictWithRequiredAlias, use_async - ) == {"prop": "2023-02-23"} # type: ignore[comparison-overlap] - - -class MyModel(BaseModel): - foo: str - - -@parametrize -@pytest.mark.asyncio -async def test_pydantic_model_to_dictionary(use_async: bool) -> None: - assert cast(Any, await transform(MyModel(foo="hi!"), Any, use_async)) == {"foo": "hi!"} - assert cast(Any, await transform(MyModel.construct(foo="hi!"), Any, use_async)) == {"foo": "hi!"} - - -@parametrize -@pytest.mark.asyncio -async def test_pydantic_empty_model(use_async: bool) -> None: - assert cast(Any, await transform(MyModel.construct(), Any, use_async)) == {} - - -@parametrize -@pytest.mark.asyncio -async def test_pydantic_unknown_field(use_async: bool) -> None: - assert cast(Any, await transform(MyModel.construct(my_untyped_field=True), Any, use_async)) == { - "my_untyped_field": True - } - - -@parametrize -@pytest.mark.asyncio -async def test_pydantic_mismatched_types(use_async: bool) -> None: - model = MyModel.construct(foo=True) - if PYDANTIC_V1: - params = await transform(model, Any, use_async) - else: - with pytest.warns(UserWarning): - params = await transform(model, Any, use_async) - assert cast(Any, params) == {"foo": True} - - -@parametrize -@pytest.mark.asyncio -async def test_pydantic_mismatched_object_type(use_async: bool) -> None: - model = MyModel.construct(foo=MyModel.construct(hello="world")) - if PYDANTIC_V1: - params = await transform(model, Any, use_async) - else: - with pytest.warns(UserWarning): - params = await transform(model, Any, use_async) - assert cast(Any, params) == {"foo": {"hello": "world"}} - - -class ModelNestedObjects(BaseModel): - nested: MyModel - - -@parametrize -@pytest.mark.asyncio -async def test_pydantic_nested_objects(use_async: bool) -> None: - model = ModelNestedObjects.construct(nested={"foo": "stainless"}) - assert isinstance(model.nested, MyModel) - assert cast(Any, await transform(model, Any, use_async)) == {"nested": {"foo": "stainless"}} - - -class ModelWithDefaultField(BaseModel): - foo: str - with_none_default: Union[str, None] = None - with_str_default: str = "foo" - - -@parametrize -@pytest.mark.asyncio -async def test_pydantic_default_field(use_async: bool) -> None: - # should be excluded when defaults are used - model = ModelWithDefaultField.construct() - assert model.with_none_default is None - assert model.with_str_default == "foo" - assert cast(Any, await transform(model, Any, use_async)) == {} - - # should be included when the default value is explicitly given - model = ModelWithDefaultField.construct(with_none_default=None, with_str_default="foo") - assert model.with_none_default is None - assert model.with_str_default == "foo" - assert cast(Any, await transform(model, Any, use_async)) == {"with_none_default": None, "with_str_default": "foo"} - - # should be included when a non-default value is explicitly given - model = ModelWithDefaultField.construct(with_none_default="bar", with_str_default="baz") - assert model.with_none_default == "bar" - assert model.with_str_default == "baz" - assert cast(Any, await transform(model, Any, use_async)) == {"with_none_default": "bar", "with_str_default": "baz"} - - -class TypedDictIterableUnion(TypedDict): - foo: Annotated[Union[Bar8, Iterable[Baz8]], PropertyInfo(alias="FOO")] - - -class Bar8(TypedDict): - foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] - - -class Baz8(TypedDict): - foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] - - -@parametrize -@pytest.mark.asyncio -async def test_iterable_of_dictionaries(use_async: bool) -> None: - assert await transform({"foo": [{"foo_baz": "bar"}]}, TypedDictIterableUnion, use_async) == { - "FOO": [{"fooBaz": "bar"}] - } - assert cast(Any, await transform({"foo": ({"foo_baz": "bar"},)}, TypedDictIterableUnion, use_async)) == { - "FOO": [{"fooBaz": "bar"}] - } - - def my_iter() -> Iterable[Baz8]: - yield {"foo_baz": "hello"} - yield {"foo_baz": "world"} - - assert await transform({"foo": my_iter()}, TypedDictIterableUnion, use_async) == { - "FOO": [{"fooBaz": "hello"}, {"fooBaz": "world"}] - } - - -@parametrize -@pytest.mark.asyncio -async def test_dictionary_items(use_async: bool) -> None: - class DictItems(TypedDict): - foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] - - assert await transform({"foo": {"foo_baz": "bar"}}, Dict[str, DictItems], use_async) == {"foo": {"fooBaz": "bar"}} - - -class TypedDictIterableUnionStr(TypedDict): - foo: Annotated[Union[str, Iterable[Baz8]], PropertyInfo(alias="FOO")] - - -@parametrize -@pytest.mark.asyncio -async def test_iterable_union_str(use_async: bool) -> None: - assert await transform({"foo": "bar"}, TypedDictIterableUnionStr, use_async) == {"FOO": "bar"} - assert cast(Any, await transform(iter([{"foo_baz": "bar"}]), Union[str, Iterable[Baz8]], use_async)) == [ - {"fooBaz": "bar"} - ] - - -class TypedDictBase64Input(TypedDict): - foo: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")] - - -@parametrize -@pytest.mark.asyncio -async def test_base64_file_input(use_async: bool) -> None: - # strings are left as-is - assert await transform({"foo": "bar"}, TypedDictBase64Input, use_async) == {"foo": "bar"} - - # pathlib.Path is automatically converted to base64 - assert await transform({"foo": SAMPLE_FILE_PATH}, TypedDictBase64Input, use_async) == { - "foo": "SGVsbG8sIHdvcmxkIQo=" - } # type: ignore[comparison-overlap] - - # io instances are automatically converted to base64 - assert await transform({"foo": io.StringIO("Hello, world!")}, TypedDictBase64Input, use_async) == { - "foo": "SGVsbG8sIHdvcmxkIQ==" - } # type: ignore[comparison-overlap] - assert await transform({"foo": io.BytesIO(b"Hello, world!")}, TypedDictBase64Input, use_async) == { - "foo": "SGVsbG8sIHdvcmxkIQ==" - } # type: ignore[comparison-overlap] - - -@parametrize -@pytest.mark.asyncio -async def test_transform_skipping(use_async: bool) -> None: - # lists of ints are left as-is - data = [1, 2, 3] - assert await transform(data, List[int], use_async) is data - - # iterables of ints are converted to a list - data = iter([1, 2, 3]) - assert await transform(data, Iterable[int], use_async) == [1, 2, 3] - - -@parametrize -@pytest.mark.asyncio -async def test_strips_notgiven(use_async: bool) -> None: - assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"} - assert await transform({"foo_bar": not_given}, Foo1, use_async) == {} - - -@parametrize -@pytest.mark.asyncio -async def test_strips_omit(use_async: bool) -> None: - assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"} - assert await transform({"foo_bar": omit}, Foo1, use_async) == {} diff --git a/tests/test_utils/test_datetime_parse.py b/tests/test_utils/test_datetime_parse.py deleted file mode 100644 index f30d48d8..00000000 --- a/tests/test_utils/test_datetime_parse.py +++ /dev/null @@ -1,110 +0,0 @@ -""" -Copied from https://github.com/pydantic/pydantic/blob/v1.10.22/tests/test_datetime_parse.py -with modifications so it works without pydantic v1 imports. -""" - -from typing import Type, Union -from datetime import date, datetime, timezone, timedelta - -import pytest - -from supermemory._utils import parse_date, parse_datetime - - -def create_tz(minutes: int) -> timezone: - return timezone(timedelta(minutes=minutes)) - - -@pytest.mark.parametrize( - "value,result", - [ - # Valid inputs - ("1494012444.883309", date(2017, 5, 5)), - (b"1494012444.883309", date(2017, 5, 5)), - (1_494_012_444.883_309, date(2017, 5, 5)), - ("1494012444", date(2017, 5, 5)), - (1_494_012_444, date(2017, 5, 5)), - (0, date(1970, 1, 1)), - ("2012-04-23", date(2012, 4, 23)), - (b"2012-04-23", date(2012, 4, 23)), - ("2012-4-9", date(2012, 4, 9)), - (date(2012, 4, 9), date(2012, 4, 9)), - (datetime(2012, 4, 9, 12, 15), date(2012, 4, 9)), - # Invalid inputs - ("x20120423", ValueError), - ("2012-04-56", ValueError), - (19_999_999_999, date(2603, 10, 11)), # just before watershed - (20_000_000_001, date(1970, 8, 20)), # just after watershed - (1_549_316_052, date(2019, 2, 4)), # nowish in s - (1_549_316_052_104, date(2019, 2, 4)), # nowish in ms - (1_549_316_052_104_324, date(2019, 2, 4)), # nowish in μs - (1_549_316_052_104_324_096, date(2019, 2, 4)), # nowish in ns - ("infinity", date(9999, 12, 31)), - ("inf", date(9999, 12, 31)), - (float("inf"), date(9999, 12, 31)), - ("infinity ", date(9999, 12, 31)), - (int("1" + "0" * 100), date(9999, 12, 31)), - (1e1000, date(9999, 12, 31)), - ("-infinity", date(1, 1, 1)), - ("-inf", date(1, 1, 1)), - ("nan", ValueError), - ], -) -def test_date_parsing(value: Union[str, bytes, int, float], result: Union[date, Type[Exception]]) -> None: - if type(result) == type and issubclass(result, Exception): # pyright: ignore[reportUnnecessaryIsInstance] - with pytest.raises(result): - parse_date(value) - else: - assert parse_date(value) == result - - -@pytest.mark.parametrize( - "value,result", - [ - # Valid inputs - # values in seconds - ("1494012444.883309", datetime(2017, 5, 5, 19, 27, 24, 883_309, tzinfo=timezone.utc)), - (1_494_012_444.883_309, datetime(2017, 5, 5, 19, 27, 24, 883_309, tzinfo=timezone.utc)), - ("1494012444", datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), - (b"1494012444", datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), - (1_494_012_444, datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), - # values in ms - ("1494012444000.883309", datetime(2017, 5, 5, 19, 27, 24, 883, tzinfo=timezone.utc)), - ("-1494012444000.883309", datetime(1922, 8, 29, 4, 32, 35, 999117, tzinfo=timezone.utc)), - (1_494_012_444_000, datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), - ("2012-04-23T09:15:00", datetime(2012, 4, 23, 9, 15)), - ("2012-4-9 4:8:16", datetime(2012, 4, 9, 4, 8, 16)), - ("2012-04-23T09:15:00Z", datetime(2012, 4, 23, 9, 15, 0, 0, timezone.utc)), - ("2012-4-9 4:8:16-0320", datetime(2012, 4, 9, 4, 8, 16, 0, create_tz(-200))), - ("2012-04-23T10:20:30.400+02:30", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(150))), - ("2012-04-23T10:20:30.400+02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(120))), - ("2012-04-23T10:20:30.400-02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(-120))), - (b"2012-04-23T10:20:30.400-02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(-120))), - (datetime(2017, 5, 5), datetime(2017, 5, 5)), - (0, datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc)), - # Invalid inputs - ("x20120423091500", ValueError), - ("2012-04-56T09:15:90", ValueError), - ("2012-04-23T11:05:00-25:00", ValueError), - (19_999_999_999, datetime(2603, 10, 11, 11, 33, 19, tzinfo=timezone.utc)), # just before watershed - (20_000_000_001, datetime(1970, 8, 20, 11, 33, 20, 1000, tzinfo=timezone.utc)), # just after watershed - (1_549_316_052, datetime(2019, 2, 4, 21, 34, 12, 0, tzinfo=timezone.utc)), # nowish in s - (1_549_316_052_104, datetime(2019, 2, 4, 21, 34, 12, 104_000, tzinfo=timezone.utc)), # nowish in ms - (1_549_316_052_104_324, datetime(2019, 2, 4, 21, 34, 12, 104_324, tzinfo=timezone.utc)), # nowish in μs - (1_549_316_052_104_324_096, datetime(2019, 2, 4, 21, 34, 12, 104_324, tzinfo=timezone.utc)), # nowish in ns - ("infinity", datetime(9999, 12, 31, 23, 59, 59, 999999)), - ("inf", datetime(9999, 12, 31, 23, 59, 59, 999999)), - ("inf ", datetime(9999, 12, 31, 23, 59, 59, 999999)), - (1e50, datetime(9999, 12, 31, 23, 59, 59, 999999)), - (float("inf"), datetime(9999, 12, 31, 23, 59, 59, 999999)), - ("-infinity", datetime(1, 1, 1, 0, 0)), - ("-inf", datetime(1, 1, 1, 0, 0)), - ("nan", ValueError), - ], -) -def test_datetime_parsing(value: Union[str, bytes, int, float], result: Union[datetime, Type[Exception]]) -> None: - if type(result) == type and issubclass(result, Exception): # pyright: ignore[reportUnnecessaryIsInstance] - with pytest.raises(result): - parse_datetime(value) - else: - assert parse_datetime(value) == result diff --git a/tests/test_utils/test_json.py b/tests/test_utils/test_json.py deleted file mode 100644 index f0672048..00000000 --- a/tests/test_utils/test_json.py +++ /dev/null @@ -1,126 +0,0 @@ -from __future__ import annotations - -import datetime -from typing import Union - -import pydantic - -from supermemory import _compat -from supermemory._utils._json import openapi_dumps - - -class TestOpenapiDumps: - def test_basic(self) -> None: - data = {"key": "value", "number": 42} - json_bytes = openapi_dumps(data) - assert json_bytes == b'{"key":"value","number":42}' - - def test_datetime_serialization(self) -> None: - dt = datetime.datetime(2023, 1, 1, 12, 0, 0) - data = {"datetime": dt} - json_bytes = openapi_dumps(data) - assert json_bytes == b'{"datetime":"2023-01-01T12:00:00"}' - - def test_pydantic_model_serialization(self) -> None: - class User(pydantic.BaseModel): - first_name: str - last_name: str - age: int - - model_instance = User(first_name="John", last_name="Kramer", age=83) - data = {"model": model_instance} - json_bytes = openapi_dumps(data) - assert json_bytes == b'{"model":{"first_name":"John","last_name":"Kramer","age":83}}' - - def test_pydantic_model_with_default_values(self) -> None: - class User(pydantic.BaseModel): - name: str - role: str = "user" - active: bool = True - score: int = 0 - - model_instance = User(name="Alice") - data = {"model": model_instance} - json_bytes = openapi_dumps(data) - assert json_bytes == b'{"model":{"name":"Alice"}}' - - def test_pydantic_model_with_default_values_overridden(self) -> None: - class User(pydantic.BaseModel): - name: str - role: str = "user" - active: bool = True - - model_instance = User(name="Bob", role="admin", active=False) - data = {"model": model_instance} - json_bytes = openapi_dumps(data) - assert json_bytes == b'{"model":{"name":"Bob","role":"admin","active":false}}' - - def test_pydantic_model_with_alias(self) -> None: - class User(pydantic.BaseModel): - first_name: str = pydantic.Field(alias="firstName") - last_name: str = pydantic.Field(alias="lastName") - - model_instance = User(firstName="John", lastName="Doe") - data = {"model": model_instance} - json_bytes = openapi_dumps(data) - assert json_bytes == b'{"model":{"firstName":"John","lastName":"Doe"}}' - - def test_pydantic_model_with_alias_and_default(self) -> None: - class User(pydantic.BaseModel): - user_name: str = pydantic.Field(alias="userName") - user_role: str = pydantic.Field(default="member", alias="userRole") - is_active: bool = pydantic.Field(default=True, alias="isActive") - - model_instance = User(userName="charlie") - data = {"model": model_instance} - json_bytes = openapi_dumps(data) - assert json_bytes == b'{"model":{"userName":"charlie"}}' - - model_with_overrides = User(userName="diana", userRole="admin", isActive=False) - data = {"model": model_with_overrides} - json_bytes = openapi_dumps(data) - assert json_bytes == b'{"model":{"userName":"diana","userRole":"admin","isActive":false}}' - - def test_pydantic_model_with_nested_models_and_defaults(self) -> None: - class Address(pydantic.BaseModel): - street: str - city: str = "Unknown" - - class User(pydantic.BaseModel): - name: str - address: Address - verified: bool = False - - if _compat.PYDANTIC_V1: - # to handle forward references in Pydantic v1 - User.update_forward_refs(**locals()) # type: ignore[reportDeprecated] - - address = Address(street="123 Main St") - user = User(name="Diana", address=address) - data = {"user": user} - json_bytes = openapi_dumps(data) - assert json_bytes == b'{"user":{"name":"Diana","address":{"street":"123 Main St"}}}' - - address_with_city = Address(street="456 Oak Ave", city="Boston") - user_verified = User(name="Eve", address=address_with_city, verified=True) - data = {"user": user_verified} - json_bytes = openapi_dumps(data) - assert ( - json_bytes == b'{"user":{"name":"Eve","address":{"street":"456 Oak Ave","city":"Boston"},"verified":true}}' - ) - - def test_pydantic_model_with_optional_fields(self) -> None: - class User(pydantic.BaseModel): - name: str - email: Union[str, None] - phone: Union[str, None] - - model_with_none = User(name="Eve", email=None, phone=None) - data = {"model": model_with_none} - json_bytes = openapi_dumps(data) - assert json_bytes == b'{"model":{"name":"Eve","email":null,"phone":null}}' - - model_with_values = User(name="Frank", email="frank@example.com", phone=None) - data = {"model": model_with_values} - json_bytes = openapi_dumps(data) - assert json_bytes == b'{"model":{"name":"Frank","email":"frank@example.com","phone":null}}' diff --git a/tests/test_utils/test_path.py b/tests/test_utils/test_path.py deleted file mode 100644 index c6dc03c1..00000000 --- a/tests/test_utils/test_path.py +++ /dev/null @@ -1,89 +0,0 @@ -from __future__ import annotations - -from typing import Any - -import pytest - -from supermemory._utils._path import path_template - - -@pytest.mark.parametrize( - "template, kwargs, expected", - [ - ("/v1/{id}", dict(id="abc"), "/v1/abc"), - ("/v1/{a}/{b}", dict(a="x", b="y"), "/v1/x/y"), - ("/v1/{a}{b}/path/{c}?val={d}#{e}", dict(a="x", b="y", c="z", d="u", e="v"), "/v1/xy/path/z?val=u#v"), - ("/{w}/{w}", dict(w="echo"), "/echo/echo"), - ("/v1/static", {}, "/v1/static"), - ("", {}, ""), - ("/v1/?q={n}&count=10", dict(n=42), "/v1/?q=42&count=10"), - ("/v1/{v}", dict(v=None), "/v1/null"), - ("/v1/{v}", dict(v=True), "/v1/true"), - ("/v1/{v}", dict(v=False), "/v1/false"), - ("/v1/{v}", dict(v=".hidden"), "/v1/.hidden"), # dot prefix ok - ("/v1/{v}", dict(v="file.txt"), "/v1/file.txt"), # dot in middle ok - ("/v1/{v}", dict(v="..."), "/v1/..."), # triple dot ok - ("/v1/{a}{b}", dict(a=".", b="txt"), "/v1/.txt"), # dot var combining with adjacent to be ok - ("/items?q={v}#{f}", dict(v=".", f=".."), "/items?q=.#.."), # dots in query/fragment are fine - ( - "/v1/{a}?query={b}", - dict(a="../../other/endpoint", b="a&bad=true"), - "/v1/..%2F..%2Fother%2Fendpoint?query=a%26bad%3Dtrue", - ), - ("/v1/{val}", dict(val="a/b/c"), "/v1/a%2Fb%2Fc"), - ("/v1/{val}", dict(val="a/b/c?query=value"), "/v1/a%2Fb%2Fc%3Fquery=value"), - ("/v1/{val}", dict(val="a/b/c?query=value&bad=true"), "/v1/a%2Fb%2Fc%3Fquery=value&bad=true"), - ("/v1/{val}", dict(val="%20"), "/v1/%2520"), # escapes escape sequences in input - # Query: slash and ? are safe, # is not - ("/items?q={v}", dict(v="a/b"), "/items?q=a/b"), - ("/items?q={v}", dict(v="a?b"), "/items?q=a?b"), - ("/items?q={v}", dict(v="a#b"), "/items?q=a%23b"), - ("/items?q={v}", dict(v="a b"), "/items?q=a%20b"), - # Fragment: slash and ? are safe - ("/docs#{v}", dict(v="a/b"), "/docs#a/b"), - ("/docs#{v}", dict(v="a?b"), "/docs#a?b"), - # Path: slash, ? and # are all encoded - ("/v1/{v}", dict(v="a/b"), "/v1/a%2Fb"), - ("/v1/{v}", dict(v="a?b"), "/v1/a%3Fb"), - ("/v1/{v}", dict(v="a#b"), "/v1/a%23b"), - # same var encoded differently by component - ( - "/v1/{v}?q={v}#{v}", - dict(v="a/b?c#d"), - "/v1/a%2Fb%3Fc%23d?q=a/b?c%23d#a/b?c%23d", - ), - ("/v1/{val}", dict(val="x?admin=true"), "/v1/x%3Fadmin=true"), # query injection - ("/v1/{val}", dict(val="x#admin"), "/v1/x%23admin"), # fragment injection - ], -) -def test_interpolation(template: str, kwargs: dict[str, Any], expected: str) -> None: - assert path_template(template, **kwargs) == expected - - -def test_missing_kwarg_raises_key_error() -> None: - with pytest.raises(KeyError, match="org_id"): - path_template("/v1/{org_id}") - - -@pytest.mark.parametrize( - "template, kwargs", - [ - ("{a}/path", dict(a=".")), - ("{a}/path", dict(a="..")), - ("/v1/{a}", dict(a=".")), - ("/v1/{a}", dict(a="..")), - ("/v1/{a}/path", dict(a=".")), - ("/v1/{a}/path", dict(a="..")), - ("/v1/{a}{b}", dict(a=".", b=".")), # adjacent vars → ".." - ("/v1/{a}.", dict(a=".")), # var + static → ".." - ("/v1/{a}{b}", dict(a="", b=".")), # empty + dot → "." - ("/v1/%2e/{x}", dict(x="ok")), # encoded dot in static text - ("/v1/%2e./{x}", dict(x="ok")), # mixed encoded ".." in static - ("/v1/.%2E/{x}", dict(x="ok")), # mixed encoded ".." in static - ("/v1/{v}?q=1", dict(v="..")), - ("/v1/{v}#frag", dict(v="..")), - ], -) -def test_dot_segment_rejected(template: str, kwargs: dict[str, Any]) -> None: - with pytest.raises(ValueError, match="dot-segment"): - path_template(template, **kwargs) diff --git a/tests/test_utils/test_proxy.py b/tests/test_utils/test_proxy.py deleted file mode 100644 index 74440350..00000000 --- a/tests/test_utils/test_proxy.py +++ /dev/null @@ -1,34 +0,0 @@ -import operator -from typing import Any -from typing_extensions import override - -from supermemory._utils import LazyProxy - - -class RecursiveLazyProxy(LazyProxy[Any]): - @override - def __load__(self) -> Any: - return self - - def __call__(self, *_args: Any, **_kwds: Any) -> Any: - raise RuntimeError("This should never be called!") - - -def test_recursive_proxy() -> None: - proxy = RecursiveLazyProxy() - assert repr(proxy) == "RecursiveLazyProxy" - assert str(proxy) == "RecursiveLazyProxy" - assert dir(proxy) == [] - assert type(proxy).__name__ == "RecursiveLazyProxy" - assert type(operator.attrgetter("name.foo.bar.baz")(proxy)).__name__ == "RecursiveLazyProxy" - - -def test_isinstance_does_not_error() -> None: - class AlwaysErrorProxy(LazyProxy[Any]): - @override - def __load__(self) -> Any: - raise RuntimeError("Mocking missing dependency") - - proxy = AlwaysErrorProxy() - assert not isinstance(proxy, dict) - assert isinstance(proxy, LazyProxy) diff --git a/tests/test_utils/test_typing.py b/tests/test_utils/test_typing.py deleted file mode 100644 index 5e9a15ec..00000000 --- a/tests/test_utils/test_typing.py +++ /dev/null @@ -1,73 +0,0 @@ -from __future__ import annotations - -from typing import Generic, TypeVar, cast - -from supermemory._utils import extract_type_var_from_base - -_T = TypeVar("_T") -_T2 = TypeVar("_T2") -_T3 = TypeVar("_T3") - - -class BaseGeneric(Generic[_T]): ... - - -class SubclassGeneric(BaseGeneric[_T]): ... - - -class BaseGenericMultipleTypeArgs(Generic[_T, _T2, _T3]): ... - - -class SubclassGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T, _T2, _T3]): ... - - -class SubclassDifferentOrderGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T2, _T, _T3]): ... - - -def test_extract_type_var() -> None: - assert ( - extract_type_var_from_base( - BaseGeneric[int], - index=0, - generic_bases=cast("tuple[type, ...]", (BaseGeneric,)), - ) - == int - ) - - -def test_extract_type_var_generic_subclass() -> None: - assert ( - extract_type_var_from_base( - SubclassGeneric[int], - index=0, - generic_bases=cast("tuple[type, ...]", (BaseGeneric,)), - ) - == int - ) - - -def test_extract_type_var_multiple() -> None: - typ = BaseGenericMultipleTypeArgs[int, str, None] - - generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) - assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int - assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str - assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) - - -def test_extract_type_var_generic_subclass_multiple() -> None: - typ = SubclassGenericMultipleTypeArgs[int, str, None] - - generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) - assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int - assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str - assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) - - -def test_extract_type_var_generic_subclass_different_ordering_multiple() -> None: - typ = SubclassDifferentOrderGenericMultipleTypeArgs[int, str, None] - - generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) - assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int - assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str - assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) diff --git a/tests/utils.py b/tests/utils.py deleted file mode 100644 index 29f5e5f2..00000000 --- a/tests/utils.py +++ /dev/null @@ -1,167 +0,0 @@ -from __future__ import annotations - -import os -import inspect -import traceback -import contextlib -from typing import Any, TypeVar, Iterator, Sequence, cast -from datetime import date, datetime -from typing_extensions import Literal, get_args, get_origin, assert_type - -from supermemory._types import Omit, NoneType -from supermemory._utils import ( - is_dict, - is_list, - is_list_type, - is_union_type, - extract_type_arg, - is_sequence_type, - is_annotated_type, - is_type_alias_type, -) -from supermemory._compat import PYDANTIC_V1, field_outer_type, get_model_fields -from supermemory._models import BaseModel - -BaseModelT = TypeVar("BaseModelT", bound=BaseModel) - - -def assert_matches_model(model: type[BaseModelT], value: BaseModelT, *, path: list[str]) -> bool: - for name, field in get_model_fields(model).items(): - field_value = getattr(value, name) - if PYDANTIC_V1: - # in v1 nullability was structured differently - # https://docs.pydantic.dev/2.0/migration/#required-optional-and-nullable-fields - allow_none = getattr(field, "allow_none", False) - else: - allow_none = False - - assert_matches_type( - field_outer_type(field), - field_value, - path=[*path, name], - allow_none=allow_none, - ) - - return True - - -# Note: the `path` argument is only used to improve error messages when `--showlocals` is used -def assert_matches_type( - type_: Any, - value: object, - *, - path: list[str], - allow_none: bool = False, -) -> None: - if is_type_alias_type(type_): - type_ = type_.__value__ - - # unwrap `Annotated[T, ...]` -> `T` - if is_annotated_type(type_): - type_ = extract_type_arg(type_, 0) - - if allow_none and value is None: - return - - if type_ is None or type_ is NoneType: - assert value is None - return - - origin = get_origin(type_) or type_ - - if is_list_type(type_): - return _assert_list_type(type_, value) - - if is_sequence_type(type_): - assert isinstance(value, Sequence) - inner_type = get_args(type_)[0] - for entry in value: # type: ignore - assert_type(inner_type, entry) # type: ignore - return - - if origin == str: - assert isinstance(value, str) - elif origin == int: - assert isinstance(value, int) - elif origin == bool: - assert isinstance(value, bool) - elif origin == float: - assert isinstance(value, float) - elif origin == bytes: - assert isinstance(value, bytes) - elif origin == datetime: - assert isinstance(value, datetime) - elif origin == date: - assert isinstance(value, date) - elif origin == object: - # nothing to do here, the expected type is unknown - pass - elif origin == Literal: - assert value in get_args(type_) - elif origin == dict: - assert is_dict(value) - - args = get_args(type_) - key_type = args[0] - items_type = args[1] - - for key, item in value.items(): - assert_matches_type(key_type, key, path=[*path, ""]) - assert_matches_type(items_type, item, path=[*path, ""]) - elif is_union_type(type_): - variants = get_args(type_) - - try: - none_index = variants.index(type(None)) - except ValueError: - pass - else: - # special case Optional[T] for better error messages - if len(variants) == 2: - if value is None: - # valid - return - - return assert_matches_type(type_=variants[not none_index], value=value, path=path) - - for i, variant in enumerate(variants): - try: - assert_matches_type(variant, value, path=[*path, f"variant {i}"]) - return - except AssertionError: - traceback.print_exc() - continue - - raise AssertionError("Did not match any variants") - elif issubclass(origin, BaseModel): - assert isinstance(value, type_) - assert assert_matches_model(type_, cast(Any, value), path=path) - elif inspect.isclass(origin) and origin.__name__ == "HttpxBinaryResponseContent": - assert value.__class__.__name__ == "HttpxBinaryResponseContent" - else: - assert None, f"Unhandled field type: {type_}" - - -def _assert_list_type(type_: type[object], value: object) -> None: - assert is_list(value) - - inner_type = get_args(type_)[0] - for entry in value: - assert_type(inner_type, entry) # type: ignore - - -@contextlib.contextmanager -def update_env(**new_env: str | Omit) -> Iterator[None]: - old = os.environ.copy() - - try: - for name, value in new_env.items(): - if isinstance(value, Omit): - os.environ.pop(name, None) - else: - os.environ[name] = value - - yield None - finally: - os.environ.clear() - os.environ.update(old) diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..5478ea0f --- /dev/null +++ b/uv.lock @@ -0,0 +1,1522 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/f4/eec0465c2f67b2664688d0240b3212d5196fd89e741df67ddb81f8d35658/aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d", size = 24757, upload-time = "2026-07-01T17:11:55.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/43/1947f06babed6b3f1d7f38b0c767f52df66bfb2bc10b468c4a7de9eceff2/aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472", size = 15038, upload-time = "2026-07-01T17:11:54.055Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11'" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/4d/4a99fb425c5e0cad715eea7bd190aff46f38b959a0a2dadb993705d34b26/aiohttp-3.14.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b", size = 765848, upload-time = "2026-07-23T01:52:08.217Z" }, + { url = "https://files.pythonhosted.org/packages/74/e8/43b85dc55b8e950dc644babe762add781319ea881b57b33d2cce12017d12/aiohttp-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a", size = 517476, upload-time = "2026-07-23T01:52:10.846Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9e/73b582c4dbbc3c12ef4473822475effaabf1f934b56f14f5b03fe5d3a2af/aiohttp-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5", size = 515334, upload-time = "2026-07-23T01:52:12.636Z" }, + { url = "https://files.pythonhosted.org/packages/79/03/e98c3c9e05a5bdf97defe5ff9169baba4f0ec9a901f2d60e0f060c2f051e/aiohttp-3.14.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f", size = 1708830, upload-time = "2026-07-23T01:52:14.538Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2c/26e60b694844dfd2176c57f913a22d0cd6a16f9ff202cbda7580d0328b98/aiohttp-3.14.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43", size = 1674012, upload-time = "2026-07-23T01:52:16.486Z" }, + { url = "https://files.pythonhosted.org/packages/38/65/672df92e3172cd876aacfa97a952ac560877eb169384b2991ac5b273de4c/aiohttp-3.14.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9", size = 1767015, upload-time = "2026-07-23T01:52:18.28Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c5/228dec7bfec1c373cc2217cdeb47d6456dcd7a13a4c55144930a75ae3851/aiohttp-3.14.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8", size = 1858700, upload-time = "2026-07-23T01:52:20.08Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ff/cb36724e8c8d17f90ada567a9ff3efe1d6e9b549fba697a242aece180f21/aiohttp-3.14.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479", size = 1714075, upload-time = "2026-07-23T01:52:22.071Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3a/296a4135c6366376263aeef54b15caca1f07676c2ae0c525d7832f2f808a/aiohttp-3.14.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b", size = 1588234, upload-time = "2026-07-23T01:52:23.757Z" }, + { url = "https://files.pythonhosted.org/packages/7d/81/9d5d853ef892dc066d1eb6db0e87a47348b920c1c879aa554612fdbd9d79/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d", size = 1677300, upload-time = "2026-07-23T01:52:25.861Z" }, + { url = "https://files.pythonhosted.org/packages/68/96/021d386ae32d9b26d4b88df2e794546232ff56bb6be952bf6be227c0bbc7/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d", size = 1691501, upload-time = "2026-07-23T01:52:28Z" }, + { url = "https://files.pythonhosted.org/packages/29/9f/af66adce26a14af135c003cbd0f44ccaa68cebd30ff8ac99ca47fb4958f7/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2", size = 1735113, upload-time = "2026-07-23T01:52:29.995Z" }, + { url = "https://files.pythonhosted.org/packages/2f/90/28c390d4c9851effe52ac25b5a2e1d92246acd00728b4fc7975dafb67484/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48", size = 1577486, upload-time = "2026-07-23T01:52:31.937Z" }, + { url = "https://files.pythonhosted.org/packages/db/c2/00e23a1bf2abb70dd353f6987db7e7f2491d0261f7363997738c71c98f95/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f", size = 1751353, upload-time = "2026-07-23T01:52:33.688Z" }, + { url = "https://files.pythonhosted.org/packages/6e/7d/d51a706a8cbfa57f0611127daf61ab3ae02ab8420b0407412079227d1c65/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32", size = 1698681, upload-time = "2026-07-23T01:52:38.167Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b0/90bd5cd9fdd9787cb4211d284d1fb8401339a933cb0227a15b71e789232f/aiohttp-3.14.3-cp310-cp310-win32.whl", hash = "sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e", size = 456733, upload-time = "2026-07-23T01:52:41.823Z" }, + { url = "https://files.pythonhosted.org/packages/d8/15/fe5b8f6a71ae112bc677163d0b0701bda5dc15005249582258ede0eb88c7/aiohttp-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c", size = 480460, upload-time = "2026-07-23T01:52:43.905Z" }, + { url = "https://files.pythonhosted.org/packages/54/00/45e98b6645cd7f00a4b78b749ebd309094b0eaeb2d2e96157eadbc0d0050/aiohttp-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb", size = 453479, upload-time = "2026-07-23T01:52:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5c/b3e4ff8ad43a8afef9602c5e90285936da1beaea8b029016b793891f03c3/aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3", size = 764250, upload-time = "2026-07-23T01:52:48.525Z" }, + { url = "https://files.pythonhosted.org/packages/0e/da/f1b384465e51449d844056b75070461da03a9a23e6c1747003695bf4172a/aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a", size = 516281, upload-time = "2026-07-23T01:52:51.047Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3f/01264f820ee2e3712a827892b1cd6ff80f3300c1fcbffbb45714a915d47a/aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8", size = 514742, upload-time = "2026-07-23T01:52:53.779Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8d/a71c6f2db52ac1ed142b133f7feddaa6b70539c3f4de24d7e226c95b794c/aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239", size = 1780613, upload-time = "2026-07-23T01:52:56.948Z" }, + { url = "https://files.pythonhosted.org/packages/a5/11/3dd9b3fb3a170f6ec9011b5291d876a6fab4086714c9e158600edf01b4fd/aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f", size = 1737688, upload-time = "2026-07-23T01:52:59.294Z" }, + { url = "https://files.pythonhosted.org/packages/6d/3e/834c26918be7d88068822b40e0db30fca50b5f4fe79104aa16a93f1d74e6/aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06", size = 1845742, upload-time = "2026-07-23T01:53:01.641Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c9/49ab8572df7d66bc13d11e31f781292badb04180dd87ba98733066c6aed7/aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929", size = 1928412, upload-time = "2026-07-23T01:53:04.018Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b9/2b8f0c0ce09c87a1daf80fd483431b56b1435d3f62789bc86f572e1245de/aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db", size = 1786220, upload-time = "2026-07-23T01:53:06.481Z" }, + { url = "https://files.pythonhosted.org/packages/85/00/9c45f81de11710460edfa1dc81317b6e882703b160926c879a9d20da9fcc/aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce", size = 1637231, upload-time = "2026-07-23T01:53:10.258Z" }, + { url = "https://files.pythonhosted.org/packages/19/ce/967d628e910756f3539c6107cb7844a1b69440dcb3029a5ee7871b09ab63/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c", size = 1753161, upload-time = "2026-07-23T01:53:13.817Z" }, + { url = "https://files.pythonhosted.org/packages/11/b2/0c3d4114f0aee4f580f5b3b4eb71b24d7a23b834ea506a4dfebe76513f35/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15", size = 1756356, upload-time = "2026-07-23T01:53:16.211Z" }, + { url = "https://files.pythonhosted.org/packages/63/5d/99e7d91c82f1399d1ae2a854e080bd1493fbc31e5e959dbc4ec33dac3bec/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c", size = 1819846, upload-time = "2026-07-23T01:53:18.289Z" }, + { url = "https://files.pythonhosted.org/packages/ad/05/d5e1cb6480eeffd3f901d40a2c5e2d1e7effdc797837da3b490272699f13/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae", size = 1628531, upload-time = "2026-07-23T01:53:23.86Z" }, + { url = "https://files.pythonhosted.org/packages/c9/90/b934682bcaefae18a9e04f3dff5b68522ba810906358ae5029b68110ea3b/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910", size = 1832712, upload-time = "2026-07-23T01:53:27.551Z" }, + { url = "https://files.pythonhosted.org/packages/21/df/6061679faaf81fac746e7307c7adb71e858071a5d34c27583afefc64f543/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7", size = 1775014, upload-time = "2026-07-23T01:53:30.223Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1d/f854878bbc69b88faefe924b619a34a6f59ec05fd387c77690667eaa75eb/aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa", size = 456006, upload-time = "2026-07-23T01:53:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/73/0c/2af9d1674baccd1dbd47282a93d660a22e57ef6167c856deb24b4214fbab/aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d", size = 481069, upload-time = "2026-07-23T01:53:39.673Z" }, + { url = "https://files.pythonhosted.org/packages/8e/76/88401ff3fc95e85c5fc38d588f36f55e61ecb64343b2bc8d69326f453cc0/aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39", size = 453021, upload-time = "2026-07-23T01:53:43.749Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" }, + { url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" }, + { url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" }, + { url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" }, + { url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" }, + { url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" }, + { url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" }, + { url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" }, + { url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" }, + { url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" }, + { url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" }, + { url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" }, + { url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" }, + { url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" }, + { url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" }, + { url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" }, + { url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" }, + { url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" }, + { url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" }, + { url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" }, + { url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" }, + { url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" }, + { url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" }, + { url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" }, + { url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" }, + { url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" }, + { url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" }, + { url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" }, + { url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" }, + { url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" }, + { url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" }, + { url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" }, + { url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" }, + { url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "anyio" +version = "4.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, +] + +[[package]] +name = "ast-serialize" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/1c/7257e6ec9382843915ce475558ce4492ccb5ed39122c256bb369c27e2ebf/ast_serialize-0.12.1.tar.gz", hash = "sha256:5285a390caf1c44368ae270f037f797b91427d138b7d43cad0f1fda4c83518d9", size = 954408, upload-time = "2026-10-03T12:25:00.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/f7/e976169da322c009bb083a52d21e88fbfe5f071e1806e8c8361ab4ac477a/ast_serialize-0.12.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:e73255c9227fd74eac8a9b55c4049e8ad7b66d1f690bf827c98a86b2e594def7", size = 897232, upload-time = "2026-10-03T12:23:21.945Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/5545f6f4d38dd41b4e2a20050967ccd722508bc90fab0dfba463d8c8b994/ast_serialize-0.12.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:4655ef993e69e01bb47d2d99647de9bbb74af03938438656832cd010d95de348", size = 1235397, upload-time = "2026-10-03T12:23:23.835Z" }, + { url = "https://files.pythonhosted.org/packages/22/19/e9b839ef9b57626e15e20dd7cf764a9a6b50f9750f86d0a49bc3a971fb72/ast_serialize-0.12.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bac5a99a2c91dd823be9b8c44645694fccbb0750773cc5b27889a9f6f22fce89", size = 1216331, upload-time = "2026-10-03T12:23:25.557Z" }, + { url = "https://files.pythonhosted.org/packages/26/2a/d054d4ff8ba42472a22e3da6eb6dee0e69a32c477b5077eefdbada99f554/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6485e681625ed7a094221f16a7ff2ef154946112266a05cf83bde50c959ef345", size = 1281194, upload-time = "2026-10-03T12:23:27.349Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/c73eddfa180a7a4c1613c0f3d3ef020b05dca9b922ac08212463c33ad11f/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a513bc6f60980d01767f7cbe39b17ce0373e722824a74ce28d6cea49ee3c8460", size = 1287170, upload-time = "2026-10-03T12:23:29.333Z" }, + { url = "https://files.pythonhosted.org/packages/94/77/39dc75d8b718844859b64a9067c9df0cfce218ca45ea215fb24a1fda3cf7/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:82866f3523d53ffca8d2a69a750bec52908b69f728012e40959bebce2620453c", size = 1531238, upload-time = "2026-10-03T12:23:30.954Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c0/6a6a6f94f45a288c4bac2eb8379a3d9654574a0f9249380ce3b07f6d64bb/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:243054a05a5190f5d087b5c8b16423e7f1cefa4991bac26e9c8ace18074b75b6", size = 1307154, upload-time = "2026-10-03T12:23:32.645Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3d/80f843892bd0f7c0d95ec5422ba3dc315c1ce011e6f08b06d5f71bd82c25/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9fbe5a3e8acddfc2fddff3dbbc7ea0e9798b3df3428f851b8abc52a3806f31", size = 1301506, upload-time = "2026-10-03T12:23:34.63Z" }, + { url = "https://files.pythonhosted.org/packages/df/cc/49a5fe852706f545e3e005584c5be89456bc637a8c9179aeaa8b9f26e8e4/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:d3d516da3463071d27e64caf54d88cba25cf4ad4afcc807e0bcf67743719f03e", size = 1297616, upload-time = "2026-10-03T12:23:36.377Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/1208c91d6e00cc43cc276bd6233c40c9b4ec3ef8537c83281dd5372cbdb8/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d6711adf11136c77e3a35517de9488a5081d1012874fae99c2876b64f4daace", size = 1354884, upload-time = "2026-10-03T12:23:38.035Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/2b5fc912ff0be64d8d61ff5dc7dc405c6311297a0e2039b848b7d14333f2/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cbe239bee4bd609186daf60b95b7b0f47146c7f7f55f6da83807d747d6fe753f", size = 1457911, upload-time = "2026-10-03T12:23:39.679Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f8/d720429bf8933efbd0cc2038c0a50b6267a585d503500845c44bc6c8ff66/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:5bbf582286c9dc6b4c544ef645dc99e4b3aa09db28892bc60344141f6926641f", size = 1563023, upload-time = "2026-10-03T12:23:41.585Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0a/99e6cc92bdbae5db60f84a14a0fb1ae77b6087e451d77808d87558162c9a/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:99e33c93efb5254a70c525b46038212371dfe5693d48eb2d0d5f17d936a263d7", size = 1556601, upload-time = "2026-10-03T12:23:43.361Z" }, + { url = "https://files.pythonhosted.org/packages/7a/05/59de9e16a2e333da534f30776d0f5e426034b64c67c17843425e3cc827d1/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:aa6c17a2b7f07e81fa8cfcc4aa7c832b3e57733853aebea113ab502f9b0963db", size = 1665039, upload-time = "2026-10-03T12:23:45.257Z" }, + { url = "https://files.pythonhosted.org/packages/33/83/35ed67a127167b484b42a071df440f84b14c0d20ea8f69dbed5cc96bfd98/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f896fa38e0af38821e1ab1425c5dee89e359623e165765bdeae7d0eb6909e76d", size = 1472263, upload-time = "2026-10-03T12:23:46.811Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b0/3ab8613bbb690297f1bb687d780a248c486df0f4131b6a82044fcb49e438/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:af699e81fd7ce80b8b03945826d8ea23dd36d072f10d4613402da597ba4ee9c6", size = 1499242, upload-time = "2026-10-03T12:23:48.531Z" }, + { url = "https://files.pythonhosted.org/packages/9d/a0/a28894d3b06f8775cea8989f371bd9f72ce562c32bc807770e7fb920ce17/ast_serialize-0.12.1-cp314-cp314t-win32.whl", hash = "sha256:10b59afc108eb285146acb23d1b5ec0fc58bb3c09cb2ab8876402df06c373c3b", size = 1119488, upload-time = "2026-10-03T12:23:50.419Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b2/0c44952f4ba4e14bb7f60a5858e2960dfefb9884e6ff007aaf64337dba5c/ast_serialize-0.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:72e871f6995a066c1b19104f8a6b5832b1163adb9a8267c2aa4711fbb0f4d1f3", size = 1157434, upload-time = "2026-10-03T12:23:52.383Z" }, + { url = "https://files.pythonhosted.org/packages/1f/1e/cb594c63f46a01d53629af1c4f9e42cd02afcea1c2fe483e12f22743ebac/ast_serialize-0.12.1-cp314-cp314t-win_arm64.whl", hash = "sha256:3398e458047d21c9bc1b323fe5aab77c608dc9ddb65b2d44deebaff503a1f1eb", size = 1129094, upload-time = "2026-10-03T12:23:54.133Z" }, + { url = "https://files.pythonhosted.org/packages/16/05/ca16884f9498386f3646bb18be59f0e31d44e992d252d7d6f5e4f8ae1ee2/ast_serialize-0.12.1-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:410233de149ab8414cb27c6fc73e9d2baa35d6f971672d540d752060d980ffb4", size = 1235686, upload-time = "2026-10-03T12:23:55.863Z" }, + { url = "https://files.pythonhosted.org/packages/29/f2/34e87ed30e292cf365523712c4bcfef1967d9c3c2749de21b1f93b1fe0f3/ast_serialize-0.12.1-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:b9a2310845302f1a6bd45ae8a67d5760211103a8d66410b854bfa440d107e093", size = 1215598, upload-time = "2026-10-03T12:23:57.48Z" }, + { url = "https://files.pythonhosted.org/packages/f8/dc/c498f41c957b6ff31b97ed8ceccf3a84f85af7debca1125183cab95bb58b/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ff65f40f49d5e1a1a043ba366081d59a4e26a9f5c1b07eb1170e172115da7ca", size = 1281911, upload-time = "2026-10-03T12:23:58.954Z" }, + { url = "https://files.pythonhosted.org/packages/dc/60/70ccefae9d88058c4c234bf0aed93f54aca36eb74087736e76e9515aee96/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:536d783c4d91331f094e0a892221e619be5ffbe6fb6640885f14d7f226ec90ca", size = 1287296, upload-time = "2026-10-03T12:24:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/08/e9/4fc697879c7128e29f9dab2ed19a9b586a56b621e5ea4aee2ae28c18e116/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c42d2d65f388d1960c5796231eb9bf5a988c46228633eb489605c4549ad16c52", size = 1532339, upload-time = "2026-10-03T12:24:02.053Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9e/9e2bd489731602a94dbd0c576ebe1cc487a2d0f6127be743f44711166f0d/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5628a12acc875fe7a167910f18d101dd101c2a7b1e6c2b6f7289ffaff25805c", size = 1308988, upload-time = "2026-10-03T12:24:03.61Z" }, + { url = "https://files.pythonhosted.org/packages/3f/69/e9cae837bd766a66db6953ffb5fc7f04b1945e02b0a9e4c6a0b6acb08f17/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e855adfa5bb982b2e6fe09056b2d584f6dd4fce085d91a07d1155683751b6b5", size = 1303412, upload-time = "2026-10-03T12:24:05.62Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1e/5ef8c62d5031d93187ed0d8dade5d942de9920c3fbd678c7652362b9a7a2/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:fafe1471e8aca6c87b4913b7b54ff97197adf702fbe28692284b929dfa62ff96", size = 1298466, upload-time = "2026-10-03T12:24:07.242Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/25ded60844a1a437edc840e597b6f81daf91dc4a26035416e14298d3a091/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a5cac246474d2a703147d1605a6ac5ba0fa9e0a443cf1cf42513adf4df02686f", size = 1355617, upload-time = "2026-10-03T12:24:09.125Z" }, + { url = "https://files.pythonhosted.org/packages/05/68/a0d3cc8d8042208a2cbef7b26483f4941b44dd5dd717bb19f20e4a4c0d66/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6e25cd319fb0d7b39fcac666784ec86708ccbc78d07a698b1400cf5ed40c045b", size = 1458949, upload-time = "2026-10-03T12:24:10.772Z" }, + { url = "https://files.pythonhosted.org/packages/df/a0/5e4d355c48a9f125b8bec7b1b98d4d2dcd8324ff1d4dfcb03678a03c1414/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:657a7354ea16ed4d29f8127ed477c6fee3915c111d135f020ca835a991438e90", size = 1562328, upload-time = "2026-10-03T12:24:12.404Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5f/3d40f6a7908200f2f0ed9ce1bad130d00e06d0405baa7918d4a4299b25dd/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:9eb9de7e59621acdb3e66984f374272b33d56d15a04763e2fd0604211e1c8303", size = 1557751, upload-time = "2026-10-03T12:24:14.455Z" }, + { url = "https://files.pythonhosted.org/packages/3f/13/d53e5a7e299d6dbaeab23a424eea2c98c821b46ed7b05abbe14743beeb63/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:09cc4d3103c1fc97f6845ba307af1db9cde5226bef47f8843220dde83f2276ba", size = 1666238, upload-time = "2026-10-03T12:24:16.171Z" }, + { url = "https://files.pythonhosted.org/packages/37/5b/7638ee3ae35a64e4467160a37dc7565cddfe2a87f06cef2fd07c93cfd503/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:c9e2a592706fd791c2271ce9c8f4e38c98d3ea0b4a86b511e09a4fe3ac44ab37", size = 1472620, upload-time = "2026-10-03T12:24:18.053Z" }, + { url = "https://files.pythonhosted.org/packages/fc/e7/6e9e621e0e4a3be5a9a5f8b6961982964d2367013dbe64feb5adaa43e56d/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:f4ac042e95a575432c1730ca4cb9183066a2074886a599f46c1f1b0955fb8198", size = 1500141, upload-time = "2026-10-03T12:24:20.036Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4f/3217da5b671711c09cc6be580095839cad539983662a6599405bd75c1a19/ast_serialize-0.12.1-cp315-abi3.abi3t-win32.whl", hash = "sha256:b3cd105995942cc6163a229674a86161ba1305f646493efd59bd6723a357ee14", size = 1118382, upload-time = "2026-10-03T12:24:21.599Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/6074cf29dca8ceff27d50e845c88e7a2eaaa7f0b6f3972909e878d844737/ast_serialize-0.12.1-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:a9cd24a26126088693ca054547ea0a391398a29cf1a3a2bec1009b4b6acc8b82", size = 1158034, upload-time = "2026-10-03T12:24:23.316Z" }, + { url = "https://files.pythonhosted.org/packages/92/a0/81ce428f9f3f1ca45f8b62c9711c30452bf8190476e8685cea0f72d8d008/ast_serialize-0.12.1-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:9649cd903db0dc047906c6dd740784a2ba665d54f7e43ba31457edbce76c9493", size = 1129437, upload-time = "2026-10-03T12:24:25.044Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d9/1c08adb90728607d0d07d188df4558ae863d688b4458087efe9fafeca458/ast_serialize-0.12.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:5ef62601db3ce5c23445132262a193075e211fb2fc87b46b7550dd351fac0976", size = 897419, upload-time = "2026-10-03T12:24:26.699Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/1eabd2c0673283054468b1c6cb539aeb877636d6c84b280279f2d7a177a9/ast_serialize-0.12.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:98d91cd3a6cb76a39512ee090a539d1e3206b732ad8150eb38918cffa1ddf515", size = 1241515, upload-time = "2026-10-03T12:24:28.493Z" }, + { url = "https://files.pythonhosted.org/packages/1b/d7/c56955934a431a0fa3e4e9aa7af4a53ceab2a61241005427545208945eb4/ast_serialize-0.12.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8a32f184ce3e4b1d0b06d642a1243281cf55b99e0323680b1b8f904029fd7700", size = 1228048, upload-time = "2026-10-03T12:24:30.556Z" }, + { url = "https://files.pythonhosted.org/packages/1e/4e/2b2ca4602baf92f842316ea617423402089df4fbd2ea42571ba28725ba46/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e62126ac2be2d9340ac1b3ee7a0466a883ecbed634cff0929c88ca0b671483b7", size = 1292087, upload-time = "2026-10-03T12:24:32.45Z" }, + { url = "https://files.pythonhosted.org/packages/d9/49/9ebd05218a87ca31f4f855d5e3df14239bba3c58f2aed9d02c7cba5d94f5/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0f93a70fa9826c04ea9f2c3a880f87f4cca09a828144ed5084682abd28110980", size = 1295355, upload-time = "2026-10-03T12:24:34.423Z" }, + { url = "https://files.pythonhosted.org/packages/dc/09/6db7c4327e7a56aba805f7190d377a159fc0bf6bdefb410dc7860624dfa3/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1858887be56a64a2aea899423dfe43787c34c75c18b0d7497de8e618d54b2790", size = 1541231, upload-time = "2026-10-03T12:24:36.05Z" }, + { url = "https://files.pythonhosted.org/packages/ed/85/7ab6097e5fe23cd4657b0e5a2fabb4f789f91e441a3ee40b3ca8b79be238/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89a2bc39a820bc7785b60c53a5742b4e8dd4c1a599294e2dd68fae545883d44a", size = 1317324, upload-time = "2026-10-03T12:24:37.737Z" }, + { url = "https://files.pythonhosted.org/packages/c0/60/58961e7fd129e226ce36788fe328d20034f3105f5d3380df690050517737/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50a9eaedf1db4857dad7cc47dd757ed70bfcc40b89d44d516c4a2f0d5033bd76", size = 1311809, upload-time = "2026-10-03T12:24:39.306Z" }, + { url = "https://files.pythonhosted.org/packages/15/c7/09d973db87d4575cba470fd80a3fa489322f7d6882546e43a4b21012468e/ast_serialize-0.12.1-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:c30b609e8fea426b310543126de876592236a25aa8ebd59f1e2b323dd52a4085", size = 1306678, upload-time = "2026-10-03T12:24:40.891Z" }, + { url = "https://files.pythonhosted.org/packages/84/27/84f69c22bcdaa5256b4fe43ff972fc117668fff8e68495807d5792eadcce/ast_serialize-0.12.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b1ad06513022cfa1337744959255af0ef16119d2beb1e547b67f37ad9433d4a", size = 1362009, upload-time = "2026-10-03T12:24:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/43/46/76ee342ef22cd6d82ccd6089d5e2f7163246de73d1816ccb4b6ec0550db6/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6add54b495e37ae3cf3a1f0d5eaba364814eb72e93026adc41b7791e4b0d45d3", size = 1467959, upload-time = "2026-10-03T12:24:44.496Z" }, + { url = "https://files.pythonhosted.org/packages/31/4d/18e48154bbf6058eed8d9b54fcebb2e130f8a380db1a2a202b4faac48626/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:5fc136cd08001b817ad0b3e7426f50a7d2b8982dc7c6491f0af78af4c3dd8672", size = 1571157, upload-time = "2026-10-03T12:24:46.156Z" }, + { url = "https://files.pythonhosted.org/packages/34/76/6b16ddf0510e713613a5f5441b13407c1bde6158df04946b9f1fdc65add3/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:45a9e6b700bbd973d49942668a5cdbeffa693f2e250b8a5409abd1fa9d351854", size = 1566873, upload-time = "2026-10-03T12:24:48.542Z" }, + { url = "https://files.pythonhosted.org/packages/75/33/9f6169ae7f60c2da4baec03450073d3f1edb39e95ab538be0d25a7d2f72e/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:a1d8267f83c613ea0a31f2518df074bd62e98a4b3a4892f6a529d74e08e02dba", size = 1673030, upload-time = "2026-10-03T12:24:50.566Z" }, + { url = "https://files.pythonhosted.org/packages/11/51/0d78755bd61d6cf8980f0cfdc7fa8ede38df46a5423c9f7a3da0cff587ec/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a0cbab9796e6ce841197feeba4008faa96b4cc7741129542fd81c882d7a4f01", size = 1480052, upload-time = "2026-10-03T12:24:52.277Z" }, + { url = "https://files.pythonhosted.org/packages/01/ae/ad4c0e5129991f2761f388420c5ded37cb134ec5882e3e59043d33c1ad87/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b4282695f1d51a3c6ef76560351bad5af880eff7d755aefea325bffb9bf68c25", size = 1508940, upload-time = "2026-10-03T12:24:53.846Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/3299182794d38815ae6e9c7ede9bb8f2e4aa93c3578bed201c1ea746643a/ast_serialize-0.12.1-cp39-abi3-win32.whl", hash = "sha256:119d1b0cadaba4a6e475f9bbe79eecc79351e373142eabe7c353f0250d70aebb", size = 1125618, upload-time = "2026-10-03T12:24:55.405Z" }, + { url = "https://files.pythonhosted.org/packages/86/14/5d4fb733c18a1d69e237c067b183842f3a7ea1c999a51ddc87093a281c88/ast_serialize-0.12.1-cp39-abi3-win_amd64.whl", hash = "sha256:3d6ed63d4fc1ec867b8cb522d58c36df0e8f05e487bea0ffd102043a37636d72", size = 1165798, upload-time = "2026-10-03T12:24:57.052Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f4/b54123680025c0b7253117418f023d1b2487f1102552acbdd9d8ee96b622/ast_serialize-0.12.1-cp39-abi3-win_arm64.whl", hash = "sha256:610a41351de68199de9a1434499083b4256c0df7658ec1cfc0a0a7b20b08d317", size = 1136560, upload-time = "2026-10-03T12:24:58.689Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, + { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, + { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-aiohttp" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/87/3b2df9732a497403e5f4bbf2ec9f25427d53cec797e83070c503649863ef/httpx_aiohttp-0.2.0.tar.gz", hash = "sha256:d4796b981f04734f1d1db9b4d9326ea16bc994f126460b93b69036262cd4a9d8", size = 195714, upload-time = "2026-07-25T07:34:12.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/e2/74b6bad3a6d342aee12d8b8d825456c02d21d72319c924326d17444c5ff7/httpx_aiohttp-0.2.0-py3-none-any.whl", hash = "sha256:ccd6eb19ba18805476096e8ef0b369a6beda3955db145a538979eface2fce7ff", size = 9732, upload-time = "2026-07-25T07:34:10.939Z" }, +] + +[[package]] +name = "idna" +version = "3.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "librt" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/f5/9dc696772d241814bacac7880bac32f2930b5a6ebc1f85317b83161a011c/librt-0.16.0.tar.gz", hash = "sha256:ac38d6d8d66bf3d744148dbbc0b8e193e195a51e364ed55e224631f5721891fc", size = 219839, upload-time = "2026-09-29T00:55:32.891Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/77/900294b3c9a81415323d81b3f3f271a7f5d7ed9d01e824fc38ffa49470b6/librt-0.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:96f576f2711f8519152ec76d0e599243555c1f07679fa73606ca8c8c868c0be6", size = 148668, upload-time = "2026-09-29T00:44:20.282Z" }, + { url = "https://files.pythonhosted.org/packages/55/3b/c301e8cc47758e4518b93e926a0111080155cc79fa03651d202ac2025065/librt-0.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2bec3818c7da7c96ceae0ef5915a3d16c52dd08f3ea913bf1fe8568c447c7978", size = 153562, upload-time = "2026-09-29T00:44:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/62/b1/4d5b98114a58bf17e3832cd33400e40f2582f68c2fb7ce25b4468cb1afd3/librt-0.16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c79520414a3fdfc6aabd7593e6169afa14d5f8d9908d4b498db068868b08dd", size = 494337, upload-time = "2026-09-29T00:44:23.096Z" }, + { url = "https://files.pythonhosted.org/packages/88/36/14223c9b0dc38982eb4f399732c8cb27b19c7e027ae0faef5ca51d08d19f/librt-0.16.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f81b5b19ce748ef68d4746656b7929762eb2fe99269b266e4be07e2ee4de7144", size = 485440, upload-time = "2026-09-29T00:44:24.555Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9e/d7155e6800d0c33e297e529b217649f7361d706491ebf9d2fc0fc627309e/librt-0.16.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c71d1b76210a36729fedfc5115069b50a3d8619054745f8758fe5d6f19e86671", size = 498363, upload-time = "2026-09-29T00:44:26.03Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ce/d5f1fb9f913ac0979d6fc5db9fb9c0bf28cc3470988528957922f15ed40c/librt-0.16.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:473eebc7866bb0a0c8849a292b5e7157c1aba5d14d0f0f610c52158d6d964262", size = 515453, upload-time = "2026-09-29T00:44:27.566Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d4/ca0fca1a27c26080d1a25b2b31f271e532078a5719878c3daca0c3d4c539/librt-0.16.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4eb1313a19847089ee81e88742abedf285c60538816640b99742d8534b81d26a", size = 509537, upload-time = "2026-09-29T00:44:29.14Z" }, + { url = "https://files.pythonhosted.org/packages/ee/c0/26807bf2221a53a2c66f2e23a5c4d57f1c0eaa4afed7cb81d841cf3d1903/librt-0.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ebefd60b42e2a82b32d136bb5f7c94eadfcd29f772b547df6f3291d1ed855a1c", size = 532541, upload-time = "2026-09-29T00:44:30.745Z" }, + { url = "https://files.pythonhosted.org/packages/0e/50/c329d509907e1bc061abeec3796f94d255d43ba25d28c3854d2138ed5ab2/librt-0.16.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6c5da27e8056439f927ea896735da60e616c477a8293feaa3233d4e7781a6726", size = 537059, upload-time = "2026-09-29T00:44:32.102Z" }, + { url = "https://files.pythonhosted.org/packages/84/d6/4571b9c9e65d98e5503734166dce2fd3e216f526dc3c59a3773faa8a60e3/librt-0.16.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:67e718c7a43f8db325abbbf1404e2d535f12f8f7a1a82259568385cc5274b82a", size = 536614, upload-time = "2026-09-29T00:44:33.653Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d3/a2ed8a292e4ab7889792a9c3f4ad5d1caf6557afce7f6b452e77f8ed20b7/librt-0.16.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:314e703f0c19320dc8094e7a784b9cf29e1b67402515abf580069a6363c0b4f1", size = 517199, upload-time = "2026-09-29T00:44:35.115Z" }, + { url = "https://files.pythonhosted.org/packages/9e/24/276e5218b4b7adb6258a42b7403cb1b7cef25616238d0c2152288a9d2f53/librt-0.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:943c6bbecbdf7fa575a4f2952fcfd848c88ef95507c3fca411e89d4ac3ff8143", size = 558746, upload-time = "2026-09-29T00:44:36.656Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5c/9156df81a1726e1efc9567379def2b8777a6d56f464d2f1b760bf951b8af/librt-0.16.0-cp310-cp310-win32.whl", hash = "sha256:7cc365f006891afb006b52d5ee5ee74c09306ffa20e2f8705a32a4450af2f3ba", size = 104424, upload-time = "2026-09-29T00:44:38.123Z" }, + { url = "https://files.pythonhosted.org/packages/0b/62/4fd172cb3f49ab2cf5ef17de519a93cfb935202c1270ba9d44c6a64178c3/librt-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:0314058469f4d2fd279ce7c62ac274ac82c3918ef7db62ef0697c4c359370155", size = 124267, upload-time = "2026-09-29T00:44:39.455Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0e/b04fd2d76b6a78a02286938251b2c119fb36ddece92c76cb4886ef7d44b8/librt-0.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fe4372c52d4849096c6cc1cda2817d293ec51440c890474ed59ef38d46556f18", size = 148058, upload-time = "2026-09-29T00:44:40.903Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fc/e1ed24aadb32d38603c0c07f579d739eb4d90e64e90d525b024e4a999b4d/librt-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c72c5295a84bd249526da9bdca38f2e176d15c31c13bb0063c5053f4ca023421", size = 153073, upload-time = "2026-09-29T00:44:42.165Z" }, + { url = "https://files.pythonhosted.org/packages/be/38/133ab655409420e0c34a9a9836a7f4e5ae75fba758d7fe177099130d58ba/librt-0.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be56ba9c884143495b517f23fe794ae367d58cd89ea0fdd6d437e3c024a87f9f", size = 493052, upload-time = "2026-09-29T00:44:44.043Z" }, + { url = "https://files.pythonhosted.org/packages/94/42/aaa663bc9421f5bf7bbbe9d3316d7219749698ec7688ac3b519770a89c04/librt-0.16.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:ef46c1a29ffb8c72e882e22618ec618778eacd0578fb22c6e7cf9c11d15f357b", size = 485506, upload-time = "2026-09-29T00:44:45.479Z" }, + { url = "https://files.pythonhosted.org/packages/7b/59/83a6eb5087ba8b45b083859e2085256963e06d4c2531944cdd962670bfb5/librt-0.16.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d3c94211ee0c4f8d649ec06b7c115c0ec4eadb873a0e3154ca15cef3f814b071", size = 498708, upload-time = "2026-09-29T00:44:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/15a8d62b2bc12935080ec7277e2e0b9b233047781eb784a51d3e25af669a/librt-0.16.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94aed6a8308818b91677957d1bd03188869cd7aeb23c5dba7912a6c0402f7602", size = 515981, upload-time = "2026-09-29T00:44:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/4f/287cc282ac5dd815204c0779e79401bb0a568c3a1335fb319bf0838fb566/librt-0.16.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:349c0bcb87ebd07481b6ff781e25cdc699723dbe2212e57dabb27f7a13b7b87d", size = 508670, upload-time = "2026-09-29T00:44:49.726Z" }, + { url = "https://files.pythonhosted.org/packages/bf/5b/ab7292add898ade0d2b5b75d32a923aae315861671aa5c39f5491309f7ae/librt-0.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:001bfd59a7d45b17e3e75f2a8c6405280b35e7b84471792778e718c4f368950e", size = 530393, upload-time = "2026-09-29T00:44:51.356Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9e/8e6bcf027d4bcbc16a4e864b3d158a1d3e5f55ffa53af0238020cad89271/librt-0.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:28e038895b998d7a0c7798922ce8a1dc157675df5cf1c9ef0aca809ed804b7a1", size = 534302, upload-time = "2026-09-29T00:44:52.893Z" }, + { url = "https://files.pythonhosted.org/packages/96/a4/8c5ab4f3b3a8e905ff8305d76976a2e666f52fe049edd398aed2402beb36/librt-0.16.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0dbe4096a7ecc00fa835d24510ad8545a4efef738dac96e0e63516783ccde905", size = 536150, upload-time = "2026-09-29T00:44:54.421Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5e/7c4bc267e3cd4b8bd288de9f62d61fff203829c9cc649ea2aa12e9218cf0/librt-0.16.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5cd5b092441053364af968ea12084692cb9d4a22f3ce9524e377880bf028761e", size = 514316, upload-time = "2026-09-29T00:44:55.998Z" }, + { url = "https://files.pythonhosted.org/packages/e0/75/c8bc6b50f43c73819334233f5a3259c81216742cef56d2140c3dd377c3f0/librt-0.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ddeb3c9dedb461bb457c6c7d9aa7fbf35329da313d1a7543d00c8d0f3473c96", size = 557731, upload-time = "2026-09-29T00:44:57.581Z" }, + { url = "https://files.pythonhosted.org/packages/93/fb/e241f91bc8de6c7ac1c6a20ea6dd381bc52312b44d711ffcd97140db1e57/librt-0.16.0-cp311-cp311-win32.whl", hash = "sha256:e05108e0849966f53a8d2d3112a7af881d0efaa479bc735bba91108f9f2350a7", size = 104922, upload-time = "2026-09-29T00:44:59.078Z" }, + { url = "https://files.pythonhosted.org/packages/d1/38/c892383d2bb1a6a80b2ec8ede84970a265a792c56614a06282bf3008517c/librt-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:5f49cff01bd608ef7d97104cb035c75455e79c2d70bf4a506cf773338ac1860d", size = 125093, upload-time = "2026-09-29T00:49:01.621Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/8bbde1ed0b21480163617648f638fdaec9374cf5e9c846ebcee34ec02a42/librt-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:1d28ae980ae2218f9c5b95d191e947296f918c9bf0b400d467a9430275bbe678", size = 115910, upload-time = "2026-09-29T00:52:40.131Z" }, + { url = "https://files.pythonhosted.org/packages/ad/76/bbdaeb87b7c47b5c7343e90222b9bfa4e4a8a83f647e02933ad0129225b1/librt-0.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe52bf4641069e7978a14253b036cb9002def1926317e710f2e249f8a8c47742", size = 151028, upload-time = "2026-09-29T00:52:41.508Z" }, + { url = "https://files.pythonhosted.org/packages/fd/0c/ab8ed3dab0085931aec4a792c7eaac8dc6c5ff4691fda3a5360d9d8a9cd2/librt-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5bcc2c4726ced915b00de0c9856a4eeabfb3fddb93e10e0b8f735b7709358b6d", size = 155283, upload-time = "2026-09-29T00:52:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/eb/36/494e79d460c80c1f030661e8287c9eca5e1ad652dc2b2180b6cd42abce0a/librt-0.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff7baa55f8e7c69851419e50a666015d02a74198716fd45c0125a2112e0a389f", size = 503185, upload-time = "2026-09-29T00:52:44.638Z" }, + { url = "https://files.pythonhosted.org/packages/9b/34/a8464038dd9db6e4381fa2b6eb73dc9a50888d77102c4c139304ac35cddc/librt-0.16.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b95d5d92ab83d39e760a52091bb1baba664f3a2351e39b1e16801e5747c2f0e9", size = 496738, upload-time = "2026-09-29T00:52:46.441Z" }, + { url = "https://files.pythonhosted.org/packages/6a/53/e0e5e334ef0c6ed27039d323819368b9ef6712be87d55ee2bf9799398afd/librt-0.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6d085d70bce51d43c5c7c36d63490770180d8779e71c49305c87b4213918de7", size = 513818, upload-time = "2026-09-29T00:52:48.068Z" }, + { url = "https://files.pythonhosted.org/packages/ad/f7/7ce72cbf19d0addd05090b339152fd0548a02562c2866a603e6e3b3da2df/librt-0.16.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36e53948e99bbe3ffea257124cfcae1cfb01831555c9a9c903c9f9a72db7fd07", size = 531952, upload-time = "2026-09-29T00:52:49.816Z" }, + { url = "https://files.pythonhosted.org/packages/83/22/0b1bcb6a8e723c8b4fd60dfc8ae8ec6461c54073fbc8685efeb8d900d407/librt-0.16.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:54d11f726aae9df5a6ffbbf0a03a52449bbac84a53ef03669cb41cdfd4ae41bf", size = 524465, upload-time = "2026-09-29T00:52:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/64/2e/e9c23b8b9df1813da1be205deca9606beb7ddd033972246cd426d05374a0/librt-0.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4323193ac0cd025f85af531df8ba91bf24d1973b401697347a6282e8fd3fcf5e", size = 543191, upload-time = "2026-09-29T00:52:53.284Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e5/6a8b21b342c03ed7e230fa3afbfd2edc58e6e87ef1f0d11fa2b9a748c252/librt-0.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e42f8e098b9c5396fefa05fb1cc7e33b0e08fc51da106b5de4a45fd22aac6743", size = 546979, upload-time = "2026-09-29T00:52:54.932Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/b5129023eced1be01e01c22757f53be551d463b1bb7264f787927404c1d6/librt-0.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:39ec1d5a14e37baf1450a6cabf03fe552340808bf1ad9d71824ab90117716459", size = 555317, upload-time = "2026-09-29T00:52:56.869Z" }, + { url = "https://files.pythonhosted.org/packages/71/89/28bba5938c725fe91f06bf93f7fa6c6b150229df53a87b454b0d5c2a796e/librt-0.16.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d1aabe3925cbb4a08d15b7b20ba4011b53019da0c4173a25155139b7b1baed65", size = 535846, upload-time = "2026-09-29T00:52:58.475Z" }, + { url = "https://files.pythonhosted.org/packages/22/92/63773026614f888c5d4e370e395ce42ca604b89f70b3acdfedbf94851b80/librt-0.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:300c3ffdc459f4a779a8411ecb188e3ac0b1ff3a3a7b099642555dedae06c69b", size = 573700, upload-time = "2026-09-29T00:53:00.127Z" }, + { url = "https://files.pythonhosted.org/packages/66/8f/347d4677eefb57cd9f8e01d95a1dcee9a91b4e44e664173c32ff6f3752e5/librt-0.16.0-cp312-cp312-win32.whl", hash = "sha256:c17194318e4c0c0348b36f36c2ec7534436fe0a4c15582403162a4f08c80797a", size = 106158, upload-time = "2026-09-29T00:53:02.031Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2c/5193dc81127cd5ddfad031391b046bf32dda219b38463ab872407ca30646/librt-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:25a58a19ea8d83b68209f04912df765e9260635ef77646542ed4b4abe6bc7940", size = 126176, upload-time = "2026-09-29T00:53:03.445Z" }, + { url = "https://files.pythonhosted.org/packages/ff/3d/9668a400c8dd81d162eba38b33fa49fa6205f1a64493570a13fbb815c3ee/librt-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7be7cf555bc30ec12622e9447299cc4a9b8ff307548b634794353db0c2065dc", size = 116572, upload-time = "2026-09-29T00:53:04.815Z" }, + { url = "https://files.pythonhosted.org/packages/46/cd/ae5e0e9dba45d1399aa04a5395bcc0bead40d9fa06dc903634a7b4d7473d/librt-0.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c5e6144e68b577f157519f2ba88ca20e3ed61c29b00e5cdfa76cd2d45acf059a", size = 151036, upload-time = "2026-09-29T00:53:06.284Z" }, + { url = "https://files.pythonhosted.org/packages/41/5a/48a16e323c5f9447a94cce7b59babf60fa04e62c3365ecf060c77ed8b320/librt-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:33f41443a1f4e1f099331b3d8120e409fbff84b9760bc1cc9ea496f37ddaa5cc", size = 155211, upload-time = "2026-09-29T00:53:07.72Z" }, + { url = "https://files.pythonhosted.org/packages/3f/29/0f59299eb4251a409b2e690ad4b7d9f8a676db829d7817ec961f32b44f7e/librt-0.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e510b7770bee609617a3374a96548eb114cae048023e3f049ee449e7ff2db32", size = 502587, upload-time = "2026-09-29T00:53:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/de/ba/d6fb4ef8d1537c396079d72289f16be7cd35a366e5065c51253fea2760b6/librt-0.16.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:efc49c462d4516b8a58b00b490078fa64689fd1fe66970cc190131d7afb8027e", size = 496146, upload-time = "2026-09-29T00:53:11.016Z" }, + { url = "https://files.pythonhosted.org/packages/52/fc/8c50dd4d7cc97c0ee8f252c8a3104980f234391cf1519b554e8b9de08b60/librt-0.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:92caf82ebef5e12d21c72242b70d1e92536f1711cf2a727a4c276de4b4469087", size = 513336, upload-time = "2026-09-29T00:53:12.655Z" }, + { url = "https://files.pythonhosted.org/packages/a9/59/16c409c56f708eda2db9a0553662845d45e3871c77d70a240dae3f3bdc56/librt-0.16.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17bac7f7a16b328fff77e440287693eb017abde913595b5827ebccbc21ecd8a6", size = 531689, upload-time = "2026-09-29T00:53:14.39Z" }, + { url = "https://files.pythonhosted.org/packages/88/82/d34772a6c29d1446dcca6e64d74062efd508523ab351aa16625a4689d5cc/librt-0.16.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5b976054553670829985ed767feb78fb6bcede0175327c4844dd5c281c1be659", size = 524537, upload-time = "2026-09-29T00:53:16.064Z" }, + { url = "https://files.pythonhosted.org/packages/77/8f/24c5631313746131ccee53bc91fdc8374f9cf25e0082a1fee9c93bb98acc/librt-0.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0058f9d68721094105917254c72ac0569117bb7b13b9769cf45d26d89f9d21cd", size = 543231, upload-time = "2026-09-29T00:53:17.939Z" }, + { url = "https://files.pythonhosted.org/packages/f2/cb/5f8e0d41dbd8b499c2265e939c31acc9ba59845565bf99539ad1c06aebcf/librt-0.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b7beaf3f4487b7d8adef1f158b49067cb4d5a19fa7a3bf31a4e7a820e435c5", size = 546499, upload-time = "2026-09-29T00:53:19.666Z" }, + { url = "https://files.pythonhosted.org/packages/be/61/063052de441d1385f59cea4223f184bf9e5d125de1ae3239b490aa1e486e/librt-0.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:468df902df016a06eb0e40b0747dc8d14e47d7a38b18b63b1fb167d85cb94d63", size = 555261, upload-time = "2026-09-29T00:53:21.292Z" }, + { url = "https://files.pythonhosted.org/packages/14/11/a2ada0529372268d6401afa9d457a095b68cd7753532b6f7f33049a19b43/librt-0.16.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:aea7b1f2b125dad5de85f049136651bff256c883c65e6b9209b2da0a1ac3cdef", size = 536029, upload-time = "2026-09-29T00:53:23.07Z" }, + { url = "https://files.pythonhosted.org/packages/23/9d/5bb6d38853382986dca702fc7e06c8256d30f5fa0676d882773b744610dc/librt-0.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a8afb6557920860b7a3a596eb804cf37e09e7cf8a803db2478c202acc72d8c2e", size = 573748, upload-time = "2026-09-29T00:53:24.696Z" }, + { url = "https://files.pythonhosted.org/packages/99/f6/0025cde35ff7f607684dc775a2b2d732cce2561cec050a6ee1fb2e1fc6fe/librt-0.16.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:77c7a2b4fe2c1369e0d5aa1cade26740a7b14be32fbc9a5535d617d20065c39d", size = 79861, upload-time = "2026-09-29T00:53:26.089Z" }, + { url = "https://files.pythonhosted.org/packages/93/93/303b8592909bd583f83f02818ffbea3f7647ca1e22b1cd5465d04b8145fe/librt-0.16.0-cp313-cp313-win32.whl", hash = "sha256:02d89c813d5ff74b17df72d3a34819d132cd168e56b81bf755b809bd9e46b8c4", size = 106236, upload-time = "2026-09-29T00:53:27.43Z" }, + { url = "https://files.pythonhosted.org/packages/cf/24/80bbb463c60ed18e29cb26aba386ddb76609580e4e7160710e550587018c/librt-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:14ed6ebe3e4f85f326d7920011ad30ff49ed9334e62cf88caef9ba973d9e3a92", size = 126180, upload-time = "2026-09-29T00:53:28.7Z" }, + { url = "https://files.pythonhosted.org/packages/43/80/b1a6fbdd7da825cdd55c71aa81eb6cfa82c513c360774152eacb50b4a771/librt-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:83d4041a3d9b2fd053a8a4e1f22878b3e5833e2712956382d5c048d791454e91", size = 116557, upload-time = "2026-09-29T00:53:30.012Z" }, + { url = "https://files.pythonhosted.org/packages/1e/93/9e0cf7da129a93c3dc7f45bc3cd4a660f2aaa995aa8a6f95c2583ef41239/librt-0.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:931a0bb0fcac88f263e269e46eb30ba8e21402cd3c62ca40cb97034c0693fab1", size = 149844, upload-time = "2026-09-29T00:53:31.391Z" }, + { url = "https://files.pythonhosted.org/packages/8f/26/8a90d2a8f2b2e471bb486b7aec117b8ae622715ed6c39853aec48ea20073/librt-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1bc17e54e5305f8d40b7ca203671ff5a9e59c1d0f8ea0f625dcca53a3984de11", size = 154111, upload-time = "2026-09-29T00:53:32.718Z" }, + { url = "https://files.pythonhosted.org/packages/35/ce/67abb46258da4d3e42ff5b141db6f38c59357bef84c7979f183b22f924f1/librt-0.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:877698bf6bca5721d8be345f2fe09778e40ecadea8b58c73075f2b1a53666bf2", size = 494234, upload-time = "2026-09-29T00:53:34.466Z" }, + { url = "https://files.pythonhosted.org/packages/12/f9/ea7162414a16f8f1bbd3b493ad6d22b926c5471916e078350bdbca8c4e5f/librt-0.16.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5981c011b306781ce561e18e14230a14524a3d8109b97553666c942c18f31a96", size = 491173, upload-time = "2026-09-29T00:53:36.124Z" }, + { url = "https://files.pythonhosted.org/packages/b1/09/9b3e869060dd33f9989b80ba4fea306f6db8cecefcdfe6d7346ef0603f65/librt-0.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:afced3dfc17cd805ecf7a3d77996a71cf5f2c75aa66eb0c21a9930f4fc992f86", size = 505547, upload-time = "2026-09-29T00:53:37.686Z" }, + { url = "https://files.pythonhosted.org/packages/50/07/79007d2165f649ea93e08c0962d1d62c70af9cde77965255095bf9d96f9a/librt-0.16.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca8052401c55d7511dda6760719fda7618067e83535d7d0010096d216c34b667", size = 523057, upload-time = "2026-09-29T00:53:39.301Z" }, + { url = "https://files.pythonhosted.org/packages/61/0c/8fbaff66d0ba376d8864653f5acce1569bae27e89648671f26f7eca67ab8/librt-0.16.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1e511762a074005bb0aa569166779834e75e438370226930d0ce1866d4b6a33b", size = 515158, upload-time = "2026-09-29T00:53:41.012Z" }, + { url = "https://files.pythonhosted.org/packages/df/2e/23ff0dece76f07a4124413a57682efa0bbeb5765ac0122bc0955513f82ca/librt-0.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1e8591bd8a5a628cd7f07954c6a1592359a878bf032957a8e9057a41d644311", size = 534183, upload-time = "2026-09-29T00:53:42.451Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/e11dea21d9a29486eba78887380374189d472734fa32cc50cb37ca44d3d0/librt-0.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4aaefb4ba6c07e1aeebb2795c8958148f1d6f9af3b555b53d23d766edb6d67a", size = 540663, upload-time = "2026-09-29T00:53:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/75/e873ae158a8b7f5359be33e7fd6c1fbe02d9a78a3e89a77de6b0e837f476/librt-0.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d92db7a0f6aee44f1baee94750457e8d2d1c6ccea41842de6268d34e8dc7eddd", size = 545908, upload-time = "2026-09-29T00:53:45.812Z" }, + { url = "https://files.pythonhosted.org/packages/ba/36/8939d3f6a93e11bd9592e6fe28d2b44f1c2dc4bed6e22e72359a91e18ffb/librt-0.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:378dfaffb38e59c24a87cde5713cd865d51ff7383fa12947f3907f306ea1ca55", size = 523302, upload-time = "2026-09-29T00:53:47.596Z" }, + { url = "https://files.pythonhosted.org/packages/71/14/35309f44a077f0f42ade0e2e7cd88c0cea760c661af205c01ea90d3c0e1f/librt-0.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3e0c39bdc85370422e8b637be76eb1fd07d30967551b03e62267dd156f553152", size = 565911, upload-time = "2026-09-29T00:53:49.278Z" }, + { url = "https://files.pythonhosted.org/packages/78/0c/df6255b94967f3159ebc46f08d8e783c12da6ee269ddb74b2efed63c640c/librt-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:1b384b90ab79a7bc30b566895809a636e0666f21f3cf12b54823d025b7e83839", size = 79119, upload-time = "2026-09-29T00:53:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/6b/44/d30d5a5461378c9d33f36736c6791c3b4c4ba4b1ffe0da7350aedcb2c9a0/librt-0.16.0-cp314-cp314-win32.whl", hash = "sha256:52327da75a94012e7f932f913d20d3876bed3c102be00e6c3e8600ff7bdd58a7", size = 100140, upload-time = "2026-09-29T00:53:52.205Z" }, + { url = "https://files.pythonhosted.org/packages/c2/98/769712f356a1e897df3581bb0c3100375d054a000de26099360ea65b5111/librt-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:3f0b8114c44b2ac06ff5dacd08e07e8e807ff4f46083f2a1602685122559be41", size = 120464, upload-time = "2026-09-29T00:53:53.495Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d3/ae2abccc8bdc8b063c1613a77686e17b74e9d3d60cfe6f12c63fa2821b9f/librt-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:8caf96a4ef8fb27d0ac0d1ad8337d26a240acd4a02fe4345d0a8f264753e8f99", size = 110889, upload-time = "2026-09-29T00:53:54.817Z" }, + { url = "https://files.pythonhosted.org/packages/e4/26/0737d4be058dd6376eade7dd8b380d4b869b6394cee929393a5a431c45bb/librt-0.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:953107e2f68d0f3512c48f898b0dbf0ce5cc52bba0f318d847c985dc555ee4cc", size = 159838, upload-time = "2026-09-29T00:53:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/01/96/9bc96531d7c620e9949af904470f02e3fa8f35129ab8e8f281c51eaa3788/librt-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad37d5b9abd49c9a655dcda7ea52a8a752884062ef1ee71ae17c2f2a0f81fe6a", size = 162018, upload-time = "2026-09-29T00:53:57.532Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fa/b0289dcb186eb3f97221ba00da5f4bd3ba7fa5e99752d48aa8d336615334/librt-0.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c4aa329c17bd1aaea4f6e89335d8ccd494b3a5830b6654462273e50e11023f0", size = 703983, upload-time = "2026-09-29T00:53:59.024Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ab/05ebbbde7530fc5eeb58fbd1581522a9f64f132fa703c4c595eed6e14760/librt-0.16.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0ead24d2562a49473dddd9efef8581f020007eb0054389c3ee3ffad38b1ca4c9", size = 683847, upload-time = "2026-09-29T00:54:00.671Z" }, + { url = "https://files.pythonhosted.org/packages/a3/75/f52aeecd4dbadbddf80725ba7de126d8bd5d0eb66247eae17a81ed90dd4d/librt-0.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02118f56a9c36ddd07dfd9b919d9ecc117ba20a90987d56aa4c429fa34509188", size = 698045, upload-time = "2026-09-29T00:54:02.247Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/fa277a835cd6eb42380591ceb85f48c5b4d2b2d7e2cb9869e1e17d24d237/librt-0.16.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e29522c62e28595ff7e324c6834ade51127707f0e255b18d1c1cf03d39c1048", size = 724640, upload-time = "2026-09-29T00:54:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/25/e4/2cf64354f3fde8ebd591b3b48f96510bee24ac5f7d1e567adbbe210abdbd/librt-0.16.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ff4b2367926b69c6215635902cccb04048e73094e9862900d27cb2c6bbff143", size = 731748, upload-time = "2026-09-29T00:54:05.741Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c9/c180af3e94e01aa529fa93d7733fec2abc47f222345400cf21d5481d5f8a/librt-0.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c6f1b27bf1632a7e016af9f145f82be95e1edd7721a646505c21059257cb5a04", size = 754960, upload-time = "2026-09-29T00:54:07.38Z" }, + { url = "https://files.pythonhosted.org/packages/95/d6/01073aa78c58f356b10d9c57b3fe9abb143df338142bcd316df417b89db0/librt-0.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5696d7f52e7b37217cb3a8f92c744fe835942602fdd4c1a8bc4741d3bfdce15e", size = 747000, upload-time = "2026-09-29T00:54:09.06Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d8/1de3783908658d697a8cfc00582f61299ffba7796d7260c112e4700b1109/librt-0.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6072e92dd876ff6ceeb6cf371e35e51f479349837391341f479b08df4564242b", size = 749569, upload-time = "2026-09-29T00:54:10.702Z" }, + { url = "https://files.pythonhosted.org/packages/b7/32/e817f66c96d6caa8bb8435ff93c4c220624d59efc506be59ab98fcd01d0c/librt-0.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:39ca4f2f2fe05de8e63493da592d84311adabe5bef52b193851981da9816b302", size = 729483, upload-time = "2026-09-29T00:54:12.25Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c9/23992ccd2b9d22798fdd0f61353183a47414e651da782ad83eb680f50833/librt-0.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9807485a908f00355820f18e91e045ffdcdc5adb68aaec40a1e2b88c5f7bba1", size = 776584, upload-time = "2026-09-29T00:54:13.837Z" }, + { url = "https://files.pythonhosted.org/packages/67/3b/e8af957f08e6e2e8e566b099e43d2748418aa565df6ee1d9d3331209fdfe/librt-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:94be5cb7bca4df6201f4183e9e4fa2086c655283d20b38cd84500a69057575a7", size = 104402, upload-time = "2026-09-29T00:54:15.568Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1c/946e6443d7cd32347a086043395e421e52fd603c9163d2ec970ceab8eed6/librt-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d46ca272b251d033dd4527b0dec5f261a28a52bd5fa0f99c117b0a1f8588cc2d", size = 126227, upload-time = "2026-09-29T00:54:17.165Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a3/bc4f9959d3c62bcbf9e5fd3470a8bbd8bf33224ed4c25d7fba173201b8ac/librt-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b9d6d4b14e92d876f8026b54c20c445f36425214c1081dc76f74e40db386b82b", size = 116493, upload-time = "2026-09-29T00:54:18.567Z" }, + { url = "https://files.pythonhosted.org/packages/b6/4b/10fdb42dfab4c1533e1570e686b18e86ff4328b406361b39fb3016667638/librt-0.16.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:6fe436af2eaf630474f491af5d032cbe45f93fcff5c3b9fe4ab194a7255b20ff", size = 149829, upload-time = "2026-09-29T00:54:19.933Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/a90ca13f1d3d91af1680000a4038536907018fbd51764767287a05d28b8b/librt-0.16.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:8ff5d26c529336be9bd7ae04483235d77778ee7d6444a95353102b542601ce81", size = 154522, upload-time = "2026-09-29T00:54:21.306Z" }, + { url = "https://files.pythonhosted.org/packages/5c/dd/bcf364eacfa070bb1fc88d503111ae7177914197ba1fa717c748926e7930/librt-0.16.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:909d8e3c1faee44cb762b1c519ff8613dcc5ceae5c99987a00917b5a31fd1d6a", size = 497593, upload-time = "2026-09-29T00:54:22.798Z" }, + { url = "https://files.pythonhosted.org/packages/18/c1/2c4e81e347bdabfe8346bfc6dc37ae5e154d61c88e30848ec0606025a5e0/librt-0.16.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:6d4a64283ee61824b5790de882bc68e2d9d7a5143537cb7a966f7354f71646d4", size = 480459, upload-time = "2026-09-29T00:54:24.364Z" }, + { url = "https://files.pythonhosted.org/packages/ab/d7/fef2a3cb8400701be496f6e459876f650b3451f407e30cb243de571ae615/librt-0.16.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5810ba811297fdf37a1531a57667cb8ace0842013ca8606bf9eb7c24cf4be154", size = 507814, upload-time = "2026-09-29T00:54:25.997Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6f/53762927a32e9dc9eb1d1c1f3528da281290c86d96929a8af652f671266e/librt-0.16.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e56aaf8c167548dc8e5d6f3bd0f48dcdd299a23c73be3f744aab79d99e9c7f5d", size = 525114, upload-time = "2026-09-29T00:54:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/6c/67/0b9d031f303c4e8c691a9a8ef9d272f13b530c11cc563b819df621b6a348/librt-0.16.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f36c58e33b304b525c6c9c5076399c6ebf1109e17b9051a05a407b091b9215b", size = 520465, upload-time = "2026-09-29T00:54:29.331Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/2056a3a85864e882eb17a203a10ddb26fa748bc9718ec67e79059ab46cae/librt-0.16.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:242e00b3d4fa37c3d3c1ca5f5c9adb7d909ddb1eac9c41f2787320d00caa0af2", size = 537257, upload-time = "2026-09-29T00:54:30.915Z" }, + { url = "https://files.pythonhosted.org/packages/54/57/e0d79790c163cbc0909e209a6f62e30bb713b647f905a176cdc64848fd4d/librt-0.16.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:0253721561787b8df8443eb347b7a6461015354e5bdd37ee38a41fef220d2bb0", size = 527442, upload-time = "2026-09-29T00:54:32.478Z" }, + { url = "https://files.pythonhosted.org/packages/aa/50/1c0c95aba7af51f4752ea34fbf2eb79b36e7cae3a72536248e8c735de735/librt-0.16.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3e483a8d69ede8067db70c0e83007423b6925de6fd53afed01d66160f2e9398c", size = 548139, upload-time = "2026-09-29T00:54:34.183Z" }, + { url = "https://files.pythonhosted.org/packages/98/91/a8a43dd5138d4f55f88846b8f0c85454a4fad69952cebfcff9948f831290/librt-0.16.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:69ba927445cfaaffb4081003ef5224c55a5c2ab67ef956f416ef744916e44121", size = 529708, upload-time = "2026-09-29T00:54:35.761Z" }, + { url = "https://files.pythonhosted.org/packages/2d/41/d5226881ab2b7c20d9d587b37bdd4a0ec8775a96d00ca87ac9f385587db4/librt-0.16.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d6a365f2ab45a984d0e00eee0dd17f599ceab8cadab6ea07b6111c8132fc0e42", size = 567781, upload-time = "2026-09-29T00:54:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/bb/bf/2345ba57a626e8c78c4ddcc724636a8df5a593fe46c1ee76bbf477e32b0d/librt-0.16.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f01f3805f2dae4781c0c34b440e31740d082950bdaf89a6f601ad589a28af57a", size = 79177, upload-time = "2026-09-29T00:54:38.792Z" }, + { url = "https://files.pythonhosted.org/packages/1e/40/99e77936cc9207f629b077bf7cbc5e2f0827cddd7c29792f04ef881bd2c3/librt-0.16.0-cp315-cp315-win32.whl", hash = "sha256:b0e3e721c75d2e79a76d4422c79d7ba705fe1bbafec907037fe7a657a480a0e3", size = 100127, upload-time = "2026-09-29T00:54:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/56/1e/801fe26bc622061b9dfd010e166d94142cb774b733217e6d98d1c0cf2638/librt-0.16.0-cp315-cp315-win_amd64.whl", hash = "sha256:bc02954b1295de798bbdb0b4e2d8a28c2117de8b5c73dcbeb27dc32572dfb971", size = 120480, upload-time = "2026-09-29T00:54:41.722Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a9/d533983055bd36e112627384c2c038845d1df882540b8bcefb566475640b/librt-0.16.0-cp315-cp315-win_arm64.whl", hash = "sha256:c5db585d43449a5f54303d4b2774e45e1babd975cfe1630a3d708c0b80c3e560", size = 110915, upload-time = "2026-09-29T00:54:43.052Z" }, + { url = "https://files.pythonhosted.org/packages/55/fe/d62238fa9c653b0e0613290349467cb65711b5800e8e474f45e942a0ad95/librt-0.16.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f06c689cb14afd9b612727553a5ec5a40febf113ca41c4413a2b0b334285884b", size = 159714, upload-time = "2026-09-29T00:54:44.497Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ac/f31efe7818700be72ba4f9af8a80fa67c39808c8d26dacc55dc1f6f35172/librt-0.16.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:13b4e8aba90b0b1c82474e9844aa9ffe7ad3faa484350e1da64cb8188d903134", size = 162086, upload-time = "2026-09-29T00:54:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/d9/16/4d7487bf86a9d7e8e18f37ba5538789233ea693637379b75701bd35ec9de/librt-0.16.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a269c46ae327d8e6f8c1f85f7516cb52c0fa48127565a1105a4f4a05ff2a0b4", size = 712727, upload-time = "2026-09-29T00:54:47.486Z" }, + { url = "https://files.pythonhosted.org/packages/77/74/50cd550ccc1a517b9ed62347625c01ddf8c4afad66490312677c011458f6/librt-0.16.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a33e0dae1f8592146a4764d54ce842b278732d21a84e17c3bbe6b1bc158a2248", size = 681106, upload-time = "2026-09-29T00:54:49.126Z" }, + { url = "https://files.pythonhosted.org/packages/df/5d/7293f712975ee6fdd2251411fd9ef1c62bc99c7b83ecbe62dbc999b1fab7/librt-0.16.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47ada6ea32636492c61aa8ad27ae3b9404bfe7a97e3ba946d1984236cc741da0", size = 705921, upload-time = "2026-09-29T00:54:50.905Z" }, + { url = "https://files.pythonhosted.org/packages/67/f7/8aab946f11d59d1bece9ffc65d994b200c789a8ca5a95c17e17e609f912a/librt-0.16.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43bd6e642d8a248c114327f98dd25ac5a7cb5aa168ef02f0559b91874df16b8", size = 731786, upload-time = "2026-09-29T00:54:52.584Z" }, + { url = "https://files.pythonhosted.org/packages/43/76/1c42ab31e7cb8384ebf6d3af607213c495222474f4940443ae7639ab7685/librt-0.16.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c3d1bb7841a816ace6449bb26d3f9560dbfa20e71c568d23f0f62bf1e68f50b1", size = 744964, upload-time = "2026-09-29T00:54:54.217Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2e/4b19982d933d2dfced671e840b219e4c1cd3f507df6e6dabb51bbd4e3850/librt-0.16.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3931f7a3db322e7f44e02a280e3949326ce9579ad388ee8d691dc7c76da9fb70", size = 765305, upload-time = "2026-09-29T00:54:55.815Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1b/e872583de2dcb3ac7746e7a2321aeeb168274f3952a87dc66e05ddb29faf/librt-0.16.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f4462528b6000afe8f16907b5c7c2553abf1df005ba5140e6eb394541c3624c3", size = 744959, upload-time = "2026-09-29T00:54:57.532Z" }, + { url = "https://files.pythonhosted.org/packages/10/de/a18c6bcfb297af2674233e90b3c3661c3a0af0f1434a0c966d2ef708a835/librt-0.16.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:80039ba9b6a7d5f1a0175a4cca6bbefead87bd854c80abad1cb30afe47a830db", size = 758138, upload-time = "2026-09-29T00:54:59.484Z" }, + { url = "https://files.pythonhosted.org/packages/22/1c/0df1d732539c297bb1a093e1fe3204d2faf76a9022cf4e1d1c2fce059790/librt-0.16.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7a1d272724b581bb6bc769dfdafed6da2ecc9886ba2450311de55a4ac2e1e9cd", size = 744268, upload-time = "2026-09-29T00:55:01.179Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f7/ccaf31331f20c91a5bd9bd48ffc3f743f9c81bb5720cc7b2a720ca204f0a/librt-0.16.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:fbe4fb8c5445f7496d7f7f6bb0807875d09d47e6771ffa175fb2df2895fb86ba", size = 785783, upload-time = "2026-09-29T00:55:02.938Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ea/7421d9e6db894cd6cd788b604b3394a163ded6b942052b6f96c23ccf08e1/librt-0.16.0-cp315-cp315t-win32.whl", hash = "sha256:375bfe6b572a8f6cfc398709356046173bf27e64c4c5edaf5f7062f051fb4bf9", size = 104355, upload-time = "2026-09-29T00:55:04.533Z" }, + { url = "https://files.pythonhosted.org/packages/85/6d/7c31a506eb847bc58aeb2402d58e17605e821f68ab5df5e66fe274a6df41/librt-0.16.0-cp315-cp315t-win_amd64.whl", hash = "sha256:bd3150023d3dc2bc70f3784e59ffa1140d56ddba3d8125b3d6f9f85221279bfc", size = 126134, upload-time = "2026-09-29T00:55:06.017Z" }, + { url = "https://files.pythonhosted.org/packages/36/69/7a5d10ac409c4da0355e054a14371871da9b5557fcc42772cd00181c6cce/librt-0.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:8ceafb70f2a4f0826f11031942e59c0728fd98da112dc346d4352bde1e486866", size = 116391, upload-time = "2026-09-29T00:55:07.484Z" }, +] + +[[package]] +name = "multidict" +version = "6.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/99/1d4d69c3512d0ddbfa3a1b69cfd9a151012ab2eb4eabbb096201b1f0b7d8/multidict-6.9.1.tar.gz", hash = "sha256:0f06e60fa190aa7abd0914c2a766736fdc8e9f34878c4346338534b73d1b20e2", size = 182404, upload-time = "2026-09-21T17:59:05.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/1c/fb2114207afc7f00ab9897ae5beeee27de5ec58ea8b9623e879f2c56f003/multidict-6.9.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:aef74e9beabbd6c4aafc091dabff86d046ccf013ce1e4396c0fbb01b4cad9de8", size = 97307, upload-time = "2026-09-21T17:54:14.957Z" }, + { url = "https://files.pythonhosted.org/packages/b7/23/1ab8c7349adbb30c8cf41b88b0794740df8ea9f7de2e382da040a2f5bd8e/multidict-6.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1ca5ebe6d454f1e5496cf052386a559f1080bf2de75bf327ebca0a6003b79f19", size = 58830, upload-time = "2026-09-21T17:54:16.261Z" }, + { url = "https://files.pythonhosted.org/packages/e0/89/b845535014a9ad50537ca50204aaf6351c5368822a3acd9da854fb2a5a34/multidict-6.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02f6d0c4b70f783305e73f9944d8efe6be1022550f0974ba0ae9d8893c0350fa", size = 57645, upload-time = "2026-09-21T17:54:17.491Z" }, + { url = "https://files.pythonhosted.org/packages/fe/4f/28ebb40a6f8e8aa7be78bcbe4bf9a1ac9518c40ef88accf2d4cd6ed7a7e0/multidict-6.9.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8050e75af7e4c6e2d5260b84eeedb618f3e452e66473432e085b5d1b81429299", size = 301535, upload-time = "2026-09-21T17:54:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/33/51/0de5fc980df34d1ffed9ef68f60ce86c2d4baa3d422598a53f8730c56757/multidict-6.9.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4736d350371337825cac1793c9f7c40701c32a03912548c2a7608e51679cbb96", size = 298796, upload-time = "2026-09-21T17:54:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/55/58/dd86067bdd2e961fdee8ec659fabc50f4e76403616b508714ec2686b0a49/multidict-6.9.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:02fe09dc197b8ae7e355371e51e5dce2f39060cd5a8badf94904527e23a3f188", size = 282392, upload-time = "2026-09-21T17:54:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/2f/52/5464a2675579e3adb48b7cd4707a59da499660f0e22c024bc7281d2d9bdf/multidict-6.9.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ad4528cdce058b684f75fad1faf4a6a6c992fe2f08376370ca66e4ce5916a84a", size = 314538, upload-time = "2026-09-21T17:54:23.366Z" }, + { url = "https://files.pythonhosted.org/packages/d3/69/7f2b3fafe064181669157ce872a097b08a0546fb6fd2481e8c5604b47ec3/multidict-6.9.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8885e3808aedbd6725b921fb67dacaae0678933561ddd47c2b01315198c70e2e", size = 315675, upload-time = "2026-09-21T17:54:24.808Z" }, + { url = "https://files.pythonhosted.org/packages/bd/53/f65ed68b96eea2b4f6fa4f9031bdfccd8f3097c429e3d371c545d1c40ca4/multidict-6.9.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3b6e6840421c83ccb60398e333b44f910b0907bb409597685ab2eedd1e22eab", size = 303685, upload-time = "2026-09-21T17:54:26.236Z" }, + { url = "https://files.pythonhosted.org/packages/1a/5f/ade05795c6fb9adfccd61695a0f0509bc793583f589c4b1b87f27215a139/multidict-6.9.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c9648ed33dc8179e4ec04bbc73bd7f0038e1e81217a69467f61f02a78bf07e88", size = 280306, upload-time = "2026-09-21T17:54:27.685Z" }, + { url = "https://files.pythonhosted.org/packages/60/b2/c996349a61e0f43c7c95877943a67476261bf906ff993670afbd33ff5bf4/multidict-6.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d9d6544790ba50438a9c1a903c3c4afb1ec8a7832db5518549275b40ef0dd4b1", size = 291691, upload-time = "2026-09-21T17:54:29.004Z" }, + { url = "https://files.pythonhosted.org/packages/0e/87/8b7016d1084854729c80acfae71c5a51ea7e7c9411d66c3e2729772f5723/multidict-6.9.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1c9f3c25df6c9d3bbae4f6fd3f514b1c5c740a2110f56ccf36069c85417e289c", size = 290357, upload-time = "2026-09-21T17:54:30.491Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/801653b6aa9343a7430cecdefa1bf71ff24432224c02f15cc15e47837652/multidict-6.9.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cf22b43f35b7dbb9f71e8ee2041b5c00029cdfc28ede3a2f31cf8906f9a6c126", size = 305831, upload-time = "2026-09-21T17:54:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f3/9fc997861920bdbf70db82bdce5c802ec6477754c4bd78cff2898e3c63f9/multidict-6.9.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8ed78ccad4c7b421804f5d524b7740946a7ef75d89dc0dac52e9d7c24c472410", size = 308995, upload-time = "2026-09-21T17:54:33.571Z" }, + { url = "https://files.pythonhosted.org/packages/ce/5f/278b5a2c99d6de27914b49158dd6f2a6481e53359142cc366dff04821a2f/multidict-6.9.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3b1aa25f01238886a6baed9e13b9a9240ed344346c79ae280ae65e8603b21d5", size = 279240, upload-time = "2026-09-21T17:54:35.025Z" }, + { url = "https://files.pythonhosted.org/packages/3d/db/0922594c601a691eb4365a83f4f92104ef87a9728d8066fa25c99349e754/multidict-6.9.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bc94a68ea5e18f8e85dc6b522bcb53093f692c8eb62b4837ac047da73956cbe4", size = 305487, upload-time = "2026-09-21T17:54:36.555Z" }, + { url = "https://files.pythonhosted.org/packages/64/cb/07bca86d4a361a87128f3019540d7689800fdcad44e3b3ad08f1f035ed74/multidict-6.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73533644e1f69ea1164d56cc6505f564c6c44072b646b66d67df2d31fed0348c", size = 301997, upload-time = "2026-09-21T17:54:37.903Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e3/99591bf626613ea35779d2460949b45e59e589dc9058d6693c923eccd4e5/multidict-6.9.1-cp310-cp310-win32.whl", hash = "sha256:66987aa68b0f7c2a1cc5f388ca962b8ed92b10f79de38d6d0d8c716154f519d9", size = 51205, upload-time = "2026-09-21T17:54:39.152Z" }, + { url = "https://files.pythonhosted.org/packages/ae/18/10c008aac77297aa618631e8726ece31668f9dc9d9c848e4b5d2e68eb3d3/multidict-6.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a32b78c1e52ebd8e247bb68300b90b233300d8816faa008ed0713bc539fb6af0", size = 59049, upload-time = "2026-09-21T17:54:40.465Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/da73a58bef2613571902a824f1e11185207ba756215c99038f6d220bc167/multidict-6.9.1-cp310-cp310-win_arm64.whl", hash = "sha256:ab64ace1a68682d191d9bedd9d4c939406ad86b1d9f628180410644249ad46c2", size = 54610, upload-time = "2026-09-21T17:54:41.605Z" }, + { url = "https://files.pythonhosted.org/packages/e3/24/3823efc330630a1f132bcd0a9b182ddec3c53f452f551c8e8d3aaf5a2d20/multidict-6.9.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:910d4260512660484c0dc1588a316fbb35a40c081c36fc51d1225351af17cfe4", size = 96739, upload-time = "2026-09-21T17:54:42.815Z" }, + { url = "https://files.pythonhosted.org/packages/6a/69/36331fe1d3aeb0c525a9972d6cf82791075a395aa96f026f060009d691ae/multidict-6.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33fa55b990f81c2927e01399ace0d18926c69d69baa8cdaa819424132fb97987", size = 58514, upload-time = "2026-09-21T17:54:44.064Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ca/5860651f782078ef13f2761c2593f4ba066d76f626b2f30f23a7f838b775/multidict-6.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:369b5aa01b241cd3fea6890bdbb11a1425d87bf1515831500d518f4223e9d72c", size = 57303, upload-time = "2026-09-21T17:54:45.304Z" }, + { url = "https://files.pythonhosted.org/packages/c7/9f/c56e2fa223cb5d182660e1a09eec38880f96b7cd22769582f74447c60418/multidict-6.9.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:803f8b575a71b1b299d677c28db0653459c79b5308874efec813f17b7457c7f1", size = 320076, upload-time = "2026-09-21T17:54:46.826Z" }, + { url = "https://files.pythonhosted.org/packages/59/97/eda4fe0cad51f96096363ed32d3e0f8df28dab00beff4d92adc9db517726/multidict-6.9.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8a65621b98984a62e59403009591b8a5a7736273aefe1cab64cfb85b365cc07", size = 315489, upload-time = "2026-09-21T17:54:48.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/14/10b1ded0a4085a51c3054125f854da4f47ce4fec724a2d2503231b83f3db/multidict-6.9.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:13849a1d4f54c3809ae721e9e83ab28f5ea602f33660cb84eb6ef261eac706c1", size = 296716, upload-time = "2026-09-21T17:54:49.749Z" }, + { url = "https://files.pythonhosted.org/packages/dc/14/04d155dd18528443cb9009f3a9f35f7417773797d072c95104ff17b8b9b7/multidict-6.9.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd9a137a4a9becda3094f3831cd026380f75f6855e051eefe4c73ade524f1cc3", size = 330244, upload-time = "2026-09-21T17:54:51.26Z" }, + { url = "https://files.pythonhosted.org/packages/8b/90/284d10b3a9e5b312a8ec5b7a175fc7d560eefdc99886320449891a197992/multidict-6.9.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9ae9614c317c50836689ce2dfde07c05fe0b16378562e2221746c3913ede3c80", size = 331288, upload-time = "2026-09-21T17:54:52.595Z" }, + { url = "https://files.pythonhosted.org/packages/27/50/f420de3683f9b047fc5588064d84043ef57a7152ef451b8373dfc7068d22/multidict-6.9.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8f1e362c9352b50ed120f001046fdbb80810c9d56580f4c3fc13bbe30823387", size = 320663, upload-time = "2026-09-21T17:54:53.91Z" }, + { url = "https://files.pythonhosted.org/packages/01/ab/0120a650d7ce4fe167299c13a13cc4ee2dc72458a9c0e9e9330b824b73c9/multidict-6.9.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dd655518f136febd96c05131a76a863e32fc2a1d7acd4e3c959e3ceb77d8345", size = 291139, upload-time = "2026-09-21T17:54:55.313Z" }, + { url = "https://files.pythonhosted.org/packages/b3/3f/c66342f73ee5cd2c9b1dda6cfce89b5f348c48921b8c8b6e59aafbbfe31a/multidict-6.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c5d675da8f1cb5650271c8ad5e95c0a3e5a183c105e72d953b12877b1c8d0fd", size = 310044, upload-time = "2026-09-21T17:54:56.745Z" }, + { url = "https://files.pythonhosted.org/packages/05/0d/e44b90d9e77e44ec3473c4c7ac7ea95764d7e8fbf6d5a085ef9bff7ce0c2/multidict-6.9.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:658f90f49cf5af2441cad0a2b801c3ef520471989a1ec55bcb25b255b2ca8d2f", size = 303279, upload-time = "2026-09-21T17:54:58.178Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/7741f7c23c211fae31ab546f0ba41569dae7671cf7ca30d5afb59e9d92aa/multidict-6.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:43124fe172ada86d03ac3c8dc8179091341f6724d5e5d5b160e1587e4cd3761b", size = 326177, upload-time = "2026-09-21T17:54:59.825Z" }, + { url = "https://files.pythonhosted.org/packages/e8/63/0cdbfbbe36316b2ef4b8e011afaca2cf291a73fe4fabf34dd0b22e3ee48a/multidict-6.9.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f0adc22a4eb31e545221d93fc73a0f6a8cc2379d0f4309f71d1d17ba938b82", size = 323526, upload-time = "2026-09-21T17:55:01.43Z" }, + { url = "https://files.pythonhosted.org/packages/98/05/0c86e9678e78f4596df0ed7f59d2aa755107da3f2a92f1b7fa150e94cbb8/multidict-6.9.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7fc59e9ba821b220944ccfe0f89c9dc4745f6d097569992356eb03869f21e953", size = 290278, upload-time = "2026-09-21T17:55:02.993Z" }, + { url = "https://files.pythonhosted.org/packages/95/8e/71bd7f43c4883cba6c5f96db88bd2311d52e74ad7680be5915938490d56f/multidict-6.9.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:87cc632c88ee5dc80e12681047839304d98ee5c9a708d686505767001c9b8b9a", size = 321142, upload-time = "2026-09-21T17:55:04.48Z" }, + { url = "https://files.pythonhosted.org/packages/2d/00/bf59d6bb22a4c152e4039574c29490caf3c65107715121807b4bd7cde244/multidict-6.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b828cf64d62dc09ac183f03c1aeedd164ade96a2ce4934109452edf29de1dd13", size = 319045, upload-time = "2026-09-21T17:55:06.119Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3c/f27f4f045198bf1c4bcd6eb3b6cf06be2dceaceb6e67222b903fce7a6749/multidict-6.9.1-cp311-cp311-win32.whl", hash = "sha256:2c1aeb92eea59d824f004341b26d5e4b47a8a441cf9726769b9a90abf9d0e08f", size = 51309, upload-time = "2026-09-21T17:55:07.531Z" }, + { url = "https://files.pythonhosted.org/packages/91/17/c2064aef64efc007bc89a571f843e5772f27946eb9bc018731bf8016e319/multidict-6.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:5f89dad732280e7a10b74d40b91364f88e13c3f2c08c2ef83a8cd42f7a61af2e", size = 59148, upload-time = "2026-09-21T17:55:08.936Z" }, + { url = "https://files.pythonhosted.org/packages/45/a1/3bab1827edd813dfb90712af5cbb7fc1473ed4d9c871d103df4f4bb950c2/multidict-6.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:5800368526647146978389dfaa46da3356291e9f0fff9a4ef12e8c2bef964a0d", size = 54258, upload-time = "2026-09-21T17:55:10.191Z" }, + { url = "https://files.pythonhosted.org/packages/d9/0d/4b5afb6d3e545c9af0cdfe2db8f6f4c6664568c23d863d888674e447e6a4/multidict-6.9.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:29138fef49828542e859828107e42e50d0e587c513b7eb4b2d92bade2b0860fe", size = 98360, upload-time = "2026-09-21T17:55:11.508Z" }, + { url = "https://files.pythonhosted.org/packages/89/ab/1b9ca66251899981b21138b87da9d5a9c2c81af12b1ea7d19466972f7fe2/multidict-6.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:19e31815d41cefc365489e591d105d2baceb2f65aa75d29471fbdbda8651e006", size = 59979, upload-time = "2026-09-21T17:55:12.866Z" }, + { url = "https://files.pythonhosted.org/packages/36/eb/6ae44062466c26c8469ef43f2481a6a48d8cea0587b2d54514ec92e2adfd/multidict-6.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6ed30be8918e18c8bed0a2e8b70639ecf02feb61ed00ca2e41cfcb2a50fa3f42", size = 57736, upload-time = "2026-09-21T17:55:14.223Z" }, + { url = "https://files.pythonhosted.org/packages/b9/5c/a67817593019257a4ac8b0d1b4c426030e637c047b0692ef405439ecea7a/multidict-6.9.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:637f4ae36264bd7b8d9a60193acddc1d735ad52e8ed53a19931ea6d921fea8e5", size = 333097, upload-time = "2026-09-21T17:55:15.591Z" }, + { url = "https://files.pythonhosted.org/packages/90/bf/599ae2e6222822d88a247a8a7ae82fe6fd25d5700757b79603d5edafe6a0/multidict-6.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35fc236507fb1b3138f0af5ecd5f94ed752d4d6d826248eae425f86204013eea", size = 334196, upload-time = "2026-09-21T17:55:17.015Z" }, + { url = "https://files.pythonhosted.org/packages/15/10/d8aac5acacbe7f5c117866c776ec26d5f15a868759b6f37ad8e7ed3b5b02/multidict-6.9.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e58952f04772f59f11c6e007471449809a30165188669bca8fdb19dde40a8f24", size = 317465, upload-time = "2026-09-21T17:55:18.412Z" }, + { url = "https://files.pythonhosted.org/packages/19/0a/714f796f7293a8b1c5c3f465a26996231d5450c54e85d64ef1258c091134/multidict-6.9.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d35a4f1c63f07fbb8c8f9946dea98b21eddf6c57421585f71d91864be3ba2a24", size = 345914, upload-time = "2026-09-21T17:55:20.306Z" }, + { url = "https://files.pythonhosted.org/packages/da/b1/e37fbf769c567be277bcf32df6234035a4384677fcc1bd852752be3d6b93/multidict-6.9.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b5f8771aaaed7f80e84a4e471d2f29ab6721e4595075e54d03ae1ed951b2000a", size = 351163, upload-time = "2026-09-21T17:55:22.21Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5b/db08419c1e1f7c9d60cfd2787b2b517d7ae4ebbda8281b48a33eb5141467/multidict-6.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:976fd7689d69ec78d67d31d38d396d8adb562f7e8368279f76aed4aa451fa06d", size = 336881, upload-time = "2026-09-21T17:55:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/9b/07/cc9bc8a62651d2d53ab93ee4993b3a71b7cb78eb8ebfc9c757a5b6698617/multidict-6.9.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:95052e8777a86bae87c0bd0b5ab22d809e3d1d02bf69e3e66ddda5ba75a05805", size = 303405, upload-time = "2026-09-21T17:55:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/b6/0c/8e912afafa70e944dbb8bec4b66ca6e008511395278c0d3dd0e89567536a/multidict-6.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1a8adfcaf96f587ab138476eaddef95f29b8a2a8a9afbfea8d2fd62180995d02", size = 324265, upload-time = "2026-09-21T17:55:26.989Z" }, + { url = "https://files.pythonhosted.org/packages/66/6a/62c2af80fb085e6234805017af857b8e913dfac7a55e9c1349c27768c58a/multidict-6.9.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1a53de2772cfb74559df2eb4456ec4eeb908435ec55a84b69370d9d745d62aa8", size = 322085, upload-time = "2026-09-21T17:55:28.732Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6b/35bf801b336fd960811207203ffdcdc24acc3558b3e7ff2e1c914b141e70/multidict-6.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f2ce963299d42fa3f22a90adc0fdf174792ffef5ff4c7ffb68260548fb05580", size = 338107, upload-time = "2026-09-21T17:55:30.303Z" }, + { url = "https://files.pythonhosted.org/packages/80/41/495ef65bf5bba29d142d81b3fe1b8154b919bed70e96491b1e93c3c26f0f/multidict-6.9.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5b30ddf7234e611ca877575b62840e6af5977f92f1f9d532eedbb05a44ff8004", size = 339631, upload-time = "2026-09-21T17:55:32.012Z" }, + { url = "https://files.pythonhosted.org/packages/19/bd/057fdff5f4e04dcd40a960e38f77d19d3c4b67dd243ffa5f43718edb29fc/multidict-6.9.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:63ada7ee2e9345695f9e9bc4c65d72222253f07b1ac94fd0e37555cc6f3c7f60", size = 302080, upload-time = "2026-09-21T17:55:33.817Z" }, + { url = "https://files.pythonhosted.org/packages/3f/de/9ace933ee8dad808632523726f42255b09600087219e3d4ead7369820910/multidict-6.9.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3c95601ed98fad3f6e2f8fe809c3b526b0fab31ef525e00a155e227f3d17f58a", size = 341107, upload-time = "2026-09-21T17:55:35.471Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ac/7c1204406097bfc5c283d4a3287d61166807d189917567df4c1318484fc4/multidict-6.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c148e8b596000dd3e4bfe206e70f3e666be18d72032e0012555f2373c52e35d6", size = 333482, upload-time = "2026-09-21T17:55:37.002Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/a70c32ea3299ebe00f44533740cb46905717c51faf29bbd3ce8bb5886d9d/multidict-6.9.1-cp312-cp312-win32.whl", hash = "sha256:f9dad513626a33670f17cddc6078e30e311f444c957e8dbfc5b2b4603c8b4edb", size = 52548, upload-time = "2026-09-21T17:55:38.529Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2e/8c9c2591df01e5692ad1bf92febb082a17b1c6c3a19aa8b7ff49988fd989/multidict-6.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:a16a1dc8529f9e734a41c3b856f3eae7ebacdc061dde3f8a844e0c7889c97203", size = 59622, upload-time = "2026-09-21T17:55:39.86Z" }, + { url = "https://files.pythonhosted.org/packages/d0/95/59f4472ec512bc180fd207899594c7803ece96262ff9aaa3a4b6d7da6940/multidict-6.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:361f7206cf341ba94fb015688f5c8b480f8e63bd58a4c14a48aeca7851a241cc", size = 54930, upload-time = "2026-09-21T17:55:41.155Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/ddf7c76860f5f553a23ad3ca38463ccf5247081618977742c8dc4625355e/multidict-6.9.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:d7bf9e43282d69561618e8a0ea33368d532ebef42f15c096f427090521dd74f3", size = 63266, upload-time = "2026-09-21T17:55:42.676Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d3/f4ae5945ea2de597eaeb4eca3a74c59783ace54482c9b49435442b63efa8/multidict-6.9.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:03d47df72f084f757c1cb771188d5f4e3a805e4abc4d67e32509272343ae9382", size = 55771, upload-time = "2026-09-21T17:55:44.016Z" }, + { url = "https://files.pythonhosted.org/packages/93/7d/15468239920040d01c686e5ae669e6382f7bf31fc3e5e0a6b8f1c3b32d3b/multidict-6.9.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6bc94fe17c3c56e5418f79515b786b101845f70609b0d19d0c1ba13448e5633a", size = 57116, upload-time = "2026-09-21T17:55:45.315Z" }, + { url = "https://files.pythonhosted.org/packages/17/1b/b958f06aac2d8b1e485eb1c105b1b159cbf3868249e5142142451577dad2/multidict-6.9.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8f2973bbd2bebd9d2e0cd6394c1292a1a19ccd56bdcbe1e174059f1a39be5b40", size = 97692, upload-time = "2026-09-21T17:55:46.674Z" }, + { url = "https://files.pythonhosted.org/packages/93/6c/d6cfe18e61010166d7237d7527c775eb9843e7078feadea45b7628751b60/multidict-6.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de7738b8c0bb74c4cc16bbd7fb49fc2bcf6430dba11b3432cd52768ae40933e8", size = 59556, upload-time = "2026-09-21T17:55:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/46/46/9b4c1127cece0289fb207d04aae5116382ddfcb2a4dc8e0cb49a33f3c7b3/multidict-6.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e5ccad4b7bac125722f48d6f862bed3b514d8526deea06316bb72f152cd30a7c", size = 57357, upload-time = "2026-09-21T17:55:49.998Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fc/c18b07100a6064573e49e538d13d47eb2b5221f48080512df78aef54ab04/multidict-6.9.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a49ff5cdb33654cb7d6a3c377aa2a83ddefaa1db31eb10bcf3c180aa84f9af8a", size = 330433, upload-time = "2026-09-21T17:55:51.435Z" }, + { url = "https://files.pythonhosted.org/packages/62/ff/52a0082adeb69656b634609d4fb0ae65456deaae4cca3d5a12656626dc50/multidict-6.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b22ff30006a2f28f8bff878fb93413cbe3a4d1fd517c28081d848c90e9cfd2c8", size = 332487, upload-time = "2026-09-21T17:55:52.937Z" }, + { url = "https://files.pythonhosted.org/packages/39/3e/80bd4729635a7af371ff4dcc312594cf96e73f98854491be8740792de430/multidict-6.9.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3adf06c66041aa21eeb8a71e82379b74773298c8e6d3d839b151aae441a99b94", size = 316911, upload-time = "2026-09-21T17:55:54.751Z" }, + { url = "https://files.pythonhosted.org/packages/15/f4/7ea4a907e053e924e2e1694d90560f79f72056504bfd5e8483695f9527d2/multidict-6.9.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:963a8d8f97057082679523d0fd4c53a38f86bc58cabe4556faef682ae53fa2fa", size = 344950, upload-time = "2026-09-21T17:55:56.5Z" }, + { url = "https://files.pythonhosted.org/packages/0d/16/9642ae41fbfbdc546aa481c2b07ae1bc087a94ab5f5020b4d9ab77da401f/multidict-6.9.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b66ccc5c2cdd26e74fa5d4c29ffae424cc6148bf93ce574821783fb3b6d452c5", size = 350264, upload-time = "2026-09-21T17:55:58.073Z" }, + { url = "https://files.pythonhosted.org/packages/93/f2/e06c8e42074d0a4b8419bfe92afbd1d262190b69549ecbcd2dffa4f103c2/multidict-6.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fd79c521f6290c69125fa2b85fa65d9e657e6a8ffaf722dc881b926bef4aa5c", size = 336213, upload-time = "2026-09-21T17:55:59.723Z" }, + { url = "https://files.pythonhosted.org/packages/98/43/d7cf9ef4700e7c25244590d1b4f20cf12cbed86beba6c17be4d9e48c1099/multidict-6.9.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d804e4caf5d5da37d6dac1325da5629ebef1e27a294c2b568b295814aa36c7b", size = 302555, upload-time = "2026-09-21T17:56:01.666Z" }, + { url = "https://files.pythonhosted.org/packages/7d/3a/54209920324bc3928f5f2ba28b01a6dd957a9d48c3aaff77e38faaf5668d/multidict-6.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bb0d664505f4b112f384cffeee82e91e3f6448d8e574989479db4439b68cba05", size = 322247, upload-time = "2026-09-21T17:56:03.265Z" }, + { url = "https://files.pythonhosted.org/packages/33/50/df96f961b178b621ab0adbf9221d4fd21534e1064fd9f5e85c1fa27fea55/multidict-6.9.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9e14d17773b1b3c758ff153659a1824608a0cb562c45f484b5ed8a433428444a", size = 321787, upload-time = "2026-09-21T17:56:04.894Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9b/37f354562a8f82f9c1f63d3a94300fc82126d50320175f0702bbd544f87c/multidict-6.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:083735b7f395894e43adb278d5dae901448883a835ff8f1977e285fefdb10418", size = 335498, upload-time = "2026-09-21T17:56:06.783Z" }, + { url = "https://files.pythonhosted.org/packages/1e/44/78e366efc6c004185295cc9eb9711c10f13eadf3b547603b9c5526329976/multidict-6.9.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b65121091567847a8cb520d364ab22ba90e00d3cc55fa9eb34bb439f0684bcd1", size = 338244, upload-time = "2026-09-21T17:56:09.162Z" }, + { url = "https://files.pythonhosted.org/packages/ad/2b/ab5bd3964691abe4d14b43bdbdb8a621b77cea2ca352dc93159ec0a4575b/multidict-6.9.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ea999ae6e80e66ad5eea287860951b033d0104ca34d6d87c7b5125ebe0e12721", size = 301641, upload-time = "2026-09-21T17:56:11.227Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a8/f6bc899f5aafaba755edc8e4bb934bc00b1e405f8d3fbc1dd10283738074/multidict-6.9.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:095d900c242e00fbe5f321ee072e7278b4153e78c5ce9c1efde167d62c1e4771", size = 340162, upload-time = "2026-09-21T17:56:13.076Z" }, + { url = "https://files.pythonhosted.org/packages/eb/03/a8fc809ef364b8c231c065ea2ea2f97ae86d4f9a0e36b5721759fd606778/multidict-6.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6441cc837aea58be7d9baef1b2383eb8311ab9303f500f99ac90b584cd78bb14", size = 332826, upload-time = "2026-09-21T17:56:15.175Z" }, + { url = "https://files.pythonhosted.org/packages/f2/27/32dde80245e2024e9bb7a6ca3c2fb7c695dc74014cba557cc35be6f91e24/multidict-6.9.1-cp313-cp313-win32.whl", hash = "sha256:9c4880d017555d70dea367dd49271830842d48e3891c2da97da7ce8c4abcee40", size = 52378, upload-time = "2026-09-21T17:56:16.959Z" }, + { url = "https://files.pythonhosted.org/packages/be/78/1bac2987edac273a6b27fa50e9204bc3466c94a7b4f6a44828d730f8810f/multidict-6.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:ac51cd64bae51c462ea58ad2492c9b8209667a4ef60c45c4a304518b67598d5d", size = 59449, upload-time = "2026-09-21T17:56:18.336Z" }, + { url = "https://files.pythonhosted.org/packages/e1/32/2a77ce19eea48cb9b3521a202b513ba3349aafa702c8317be0b645ffb74b/multidict-6.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:37a9ebe00c698279213d56e6c64e1962ab1e092918270649b397cac3dc196ca4", size = 54713, upload-time = "2026-09-21T17:56:19.774Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0f/c6041015aa2cdc11e1dff0cae58898ae5525d19e35efd65a4ecf3c6ea235/multidict-6.9.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:fc0dcb22fa9aeabfe3fa4e0430099acff985ec5d77a851382f76cc6146e780e5", size = 62336, upload-time = "2026-09-21T17:56:21.181Z" }, + { url = "https://files.pythonhosted.org/packages/23/db/95bd0afb130a0149c955f1f066143528d60864e64f41ff5a1e1313609361/multidict-6.9.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:024123f0ab402ab33828e24eb80fa8f25167d0d3783ba5f287e39ed741e6abf9", size = 54892, upload-time = "2026-09-21T17:56:22.63Z" }, + { url = "https://files.pythonhosted.org/packages/94/cc/8aa34ae8d09498e8da003f485c03371b55cf1bc6d56cff861b3a7de61251/multidict-6.9.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:af1b5a92315048c3e36bbebfa7d4760a9c3e910bc4166f11b20d77d20d6bcfca", size = 56251, upload-time = "2026-09-21T17:56:24.36Z" }, + { url = "https://files.pythonhosted.org/packages/52/48/22baa3b95375096369b04c4348af14139570f302f951773d2c68bf0ccf2e/multidict-6.9.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b2483da477932ad1983d1d33c18bc3771c6fb00cfbaaed70a875fd547ef8e840", size = 96345, upload-time = "2026-09-21T17:56:25.777Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3e/b96779dcac28ec6d491fd821112a0156b519b6701bba186cdf6f6df737f4/multidict-6.9.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db4d697b18b6ef5528b1f36bfa25072cd2a421869f5963bc0e92c8a34b9e2800", size = 58901, upload-time = "2026-09-21T17:56:27.364Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b4/8e6d950f02ca6ecc00ecf671f2ddb5dab7017671a8d197326f75621f5786/multidict-6.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:854fd2f1bc6e8a56b89910b5cd7261a8b40f13ebb31572da985ce59c7da0886d", size = 56491, upload-time = "2026-09-21T17:56:28.901Z" }, + { url = "https://files.pythonhosted.org/packages/ed/8f/212ed7e03282c7bc271e3796bf985b53d5727c2827361e3dd9c5ee6b093f/multidict-6.9.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:03731f6fc036180700c9dc2308205a48e5ca6f3ff03087739ab746f294022201", size = 332297, upload-time = "2026-09-21T17:56:30.495Z" }, + { url = "https://files.pythonhosted.org/packages/a0/97/555aab000e03ecba85c0a1837fd5674d8bf10949490df7800da3a40660ce/multidict-6.9.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989261c5f1735a165f2e4e87cf6d5f17ab734fa18f9ad0383d5adfcdaefce701", size = 329197, upload-time = "2026-09-21T17:56:32.652Z" }, + { url = "https://files.pythonhosted.org/packages/3d/25/f3539b5bdc147beba66015795dfe589eedcc580348fb1a8c69b1df4196b5/multidict-6.9.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b4c9e5d05b126b267ac048a89a0e2d9b48b1b648add62d4872906304a8590610", size = 308456, upload-time = "2026-09-21T17:56:34.657Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7d/4665cad8fc326787d218879be879d86dfe32f006c1f88ae47b04d3b52fd5/multidict-6.9.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9d0a21cf76153de8f2d96a877991d6bc59b9ab5180b949e50db73cc193b4a694", size = 343382, upload-time = "2026-09-21T17:56:36.606Z" }, + { url = "https://files.pythonhosted.org/packages/41/01/e1e9abe27f492b22dede492bc423015597412b14ce074f7475f4ecd80185/multidict-6.9.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7386aa18d98d6b8af44b92173654ec469237fda35f8e8523e43b581a86f476a", size = 346848, upload-time = "2026-09-21T17:56:38.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/8d54118bc228e64e1087f1729647b93bbea225eda4a3f914663a6f410bca/multidict-6.9.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b69651732c64afb691e50cdc3387cae305e0eeff8804fe3e3ce203876494932a", size = 333612, upload-time = "2026-09-21T17:56:40.041Z" }, + { url = "https://files.pythonhosted.org/packages/76/e1/9509dafc1fc68948e75842a74533ddfa64af57f2b17810e9cacfa68a829b/multidict-6.9.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1fee9a16d88a1c4865610de31ef5c666671020d7a81d1f510eaf3c97d00ebeba", size = 304246, upload-time = "2026-09-21T17:56:42.313Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/96b93808e1bec0b51d49d89859e49b82a94f7b2e58eca0fc44ffcafd3ae5/multidict-6.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7814bbee202acd3bd240204c17d8b87a4c48c81064fd8674dbe527d94d5a4290", size = 321983, upload-time = "2026-09-21T17:56:44.399Z" }, + { url = "https://files.pythonhosted.org/packages/d9/1c/08e5184df026b9d8e05a6dce5da22628e46bc4a7c7f03b8bf3c6939a8c46/multidict-6.9.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:95f273bea318a194f656527ee2ed19494327bc500b8e87d9358e3222579aa28d", size = 315927, upload-time = "2026-09-21T17:56:46.208Z" }, + { url = "https://files.pythonhosted.org/packages/d7/5d/9ff39e7387f6fcc9a81af5dbf055fde08149849a04f8df10ca5b086b0d39/multidict-6.9.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:73bcafa21a78d0776b3ee7cd2a63c66f968eb7db8e8d594e32d7329950f6e828", size = 336827, upload-time = "2026-09-21T17:56:48.021Z" }, + { url = "https://files.pythonhosted.org/packages/5a/be/30fbb2cab22203128928066de94be247dc9a3230bf2bfc54437ac3b5f9f7/multidict-6.9.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1a938761c77e0e6edb0c93d02f4e988d44a69e5195e5a3b893e5553311347132", size = 336317, upload-time = "2026-09-21T17:56:49.778Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f0/2480aef6b7d8e7ab89b064ae14f12ec069570dab3a5c13e0155912293247/multidict-6.9.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:37245ca4105386194dd1d292a6f2aae09bfe1bd7ac6f9ec25093e3cf8e9b143b", size = 303258, upload-time = "2026-09-21T17:56:51.829Z" }, + { url = "https://files.pythonhosted.org/packages/4c/04/dbe59cf4ea3778600e8e70e30b31aa9118ad6da2844bc9f717804cbaa000/multidict-6.9.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f0700527dd5bfa8b7204b08330542f4f388899d3c14d885d8a368992e0eb562d", size = 337232, upload-time = "2026-09-21T17:56:53.537Z" }, + { url = "https://files.pythonhosted.org/packages/f4/8c/abe55548f06f12878588f08eb803c5a391283f01243f784cb00adac79231/multidict-6.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6b7e54fd883671d1a8810704851c044b7173287c552b9f5c3d9e0eb9f00ae194", size = 330261, upload-time = "2026-09-21T17:56:55.335Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f1/ba67f668478f152bdfa18abe72e7588bd078bf8b42c172fcf542fcf45826/multidict-6.9.1-cp314-cp314-win32.whl", hash = "sha256:e81ae656b9935ac4528a71f96bb7a14d949778ed1897c573d3e7ebb9187f8841", size = 51715, upload-time = "2026-09-21T17:56:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/014bb2128aedc8157d2848f0ec8a0209c6b822fb88deea012834e1c4a4e8/multidict-6.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:acddcac38adc8342ba48aba98896faa7928854bebb62542362138655b5367ee3", size = 58621, upload-time = "2026-09-21T17:56:58.531Z" }, + { url = "https://files.pythonhosted.org/packages/66/34/8f43c03c2d8821a2320b8c560d8a49d0be2920627edfac3f546922f91969/multidict-6.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:32217133dddc58c927805cf6c0731d8144584176b768042ee886d51e71860bc9", size = 53959, upload-time = "2026-09-21T17:57:00.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/64/4f2a5eeea10b6d42634ba168ec6efc732ef218772e57b8239cd485c202f6/multidict-6.9.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0b2fb8c349d1103863750b5d8cb5ace766917f4b35f3d883c8f778853eaa9f76", size = 107694, upload-time = "2026-09-21T17:57:01.785Z" }, + { url = "https://files.pythonhosted.org/packages/32/ce/c68d08ac2096c1528ff20d77fd10f7e401db3e534ac5a1c7726e36a77c2f/multidict-6.9.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:56d834b74c993a7d7cb2b8ab33a0d55c3e0d4a3d2f2da2808a4ad3d79189711b", size = 64766, upload-time = "2026-09-21T17:57:03.52Z" }, + { url = "https://files.pythonhosted.org/packages/3a/9a/9f5c270c88fe289b4876fe28f9932a70e015a7916aff1e5b0325f7597b11/multidict-6.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:783ba7d845d79ce976afd9c1e91a4e5714671defa198ee789e8b23316083a485", size = 62723, upload-time = "2026-09-21T17:57:05.104Z" }, + { url = "https://files.pythonhosted.org/packages/7c/63/c9a0b131354dd949426166cef712d708f436a8cf261114f8d5d2aec440ca/multidict-6.9.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d718fa1b5f0d0dd75e86fbbc5b0c93ea3a5d65216c85c61cd5d7cdddfe08455", size = 290445, upload-time = "2026-09-21T17:57:06.687Z" }, + { url = "https://files.pythonhosted.org/packages/9d/41/ed27d1d20ba91f4e13e8f7dcfaa592614ae0e1d5f45e03171d23c3b098ac/multidict-6.9.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a313bad717dde740959d50850c315b75fd4eb0c5e6b4dc8535db0f1c369be125", size = 304375, upload-time = "2026-09-21T17:57:08.717Z" }, + { url = "https://files.pythonhosted.org/packages/66/6f/a7b26167173c3b73d6e5ebb0856c5154ad005156c3a2fa4062329921819c/multidict-6.9.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ef01d29fca550ab871fd99154f82c6472aacf8e7def272dfb07b460123850390", size = 281746, upload-time = "2026-09-21T17:57:10.713Z" }, + { url = "https://files.pythonhosted.org/packages/34/1d/da27bdd49b0cb84263288f5f48dbd915f2d17fd228fee2078ca607e80277/multidict-6.9.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f3bd290711c6e9486173a6ee7cd4e7f00c3971c7908c1ec1b6e5437c5e4c6f9", size = 311499, upload-time = "2026-09-21T17:57:12.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/36/ab0299a7abf53e38ff75d96c64480f1e2ff8edde985f93f7a1ad5e08bd94/multidict-6.9.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:81cdc537e0a42e3c0170752fcadbe450246d5c3b4b7231d6eb9672456605ac94", size = 313553, upload-time = "2026-09-21T17:57:14.548Z" }, + { url = "https://files.pythonhosted.org/packages/5a/44/e21be702efc9c9ba097c8be874c68860b1cb6cf9c85611263039bf95b37e/multidict-6.9.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b193bf7a443c97d81c47052f60c486071a4bdef4a573fa2514f920089414d45", size = 299898, upload-time = "2026-09-21T17:57:16.192Z" }, + { url = "https://files.pythonhosted.org/packages/00/8d/e93bc0dc947c7f872e502cfaa7ad9d98f001e1c93cfa6f4940457a6e187a/multidict-6.9.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7a0c98f6a636adf0d7edd60c61589eaf52d149239af754d0bfeb0effedba53a8", size = 273054, upload-time = "2026-09-21T17:57:17.793Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c9/945b44493fbe7af26ffe1e43c506b376daeab070fc1a0923bef9bbf572eb/multidict-6.9.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6e6b7e3a1520c39a2772f414cd9ddf5995f77e4e823dfa1522af383addd465a3", size = 294751, upload-time = "2026-09-21T17:57:19.631Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/0969a9b38b814e5fa3af1b0a57708c49b3d9b3d55c208bac6e98b87bb028/multidict-6.9.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ca65cced0d67a9039e93bcd98a369920e499bf98296844ff56ead01c9085321f", size = 284523, upload-time = "2026-09-21T17:57:21.313Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1e/c32ddf53234c228b1a374ddabca19f542ac7f32752f5155982abe2304833/multidict-6.9.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:372c063f37480f62c1ae32dc3a1a0a5942b180c883f99789075a8a8ec3c4e709", size = 290275, upload-time = "2026-09-21T17:57:23.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/09/7a99a3daafd987330abb865a2d36e432fd8bf18e790ddda55c5de15eb98f/multidict-6.9.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:827c92145b3b976b39430129c89d213b250dacbe5fce678e9a03940e6e848983", size = 303350, upload-time = "2026-09-21T17:57:25.086Z" }, + { url = "https://files.pythonhosted.org/packages/2e/09/7e6ebaecc4e226d7fe27973c271d58b75c14c24e84c23736d21dc39997dd/multidict-6.9.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9b0124b9c17e9890f0819b2e7a5f65ec9a2f5aabd6c8e7ad090c1675cf70dee6", size = 272232, upload-time = "2026-09-21T17:57:27.193Z" }, + { url = "https://files.pythonhosted.org/packages/1f/6e/420e9e879b21cbdb5036214ee238efabcfab7bf262d3fc124132050f79ff/multidict-6.9.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:23136f5a564654eb61061ec6d5620a4c1ea32c8f552b65e9982a12b72bff601b", size = 300525, upload-time = "2026-09-21T17:57:29.004Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7f/c6b0896850b3ceb190ca302760d63148843371f027c5dde3d1d732343a08/multidict-6.9.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f2524ec55b3e65cbe235a8b3c36e2af3b635be02ed05e20c94e70c5c943c009e", size = 295933, upload-time = "2026-09-21T17:57:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/29/f2/f0ff3384a756227741c72f4ee94cadf1f9900df0ae961e48639f96115b1c/multidict-6.9.1-cp314-cp314t-win32.whl", hash = "sha256:35ba0263bae5dd3ad5aad767cc9afc01a8598c7dae30f1b3b2de98b1b32c28bd", size = 57776, upload-time = "2026-09-21T17:57:32.835Z" }, + { url = "https://files.pythonhosted.org/packages/8d/87/013e1eed30ad46bec1eb68947bc9caf55ba2fd0d6aed04fe964e3b44c23b/multidict-6.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:7c8d5882ba25ac0282258be435d8a05aa0cbcacfce15799154a338f847f159b9", size = 64881, upload-time = "2026-09-21T17:57:34.489Z" }, + { url = "https://files.pythonhosted.org/packages/7f/95/64dd049bb4c53426e2fee7afb874daaa8b4426c41878785883a94511ecf8/multidict-6.9.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ac348379cf4de5538a0a213be1532d289aa801ec5d267c5909446b9ea2f8e2c3", size = 58582, upload-time = "2026-09-21T17:57:36.19Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2c/26bc1723592cfe965b575652a7749026474b0625b04b04564e1fba265dc5/multidict-6.9.1-cp315-cp315-android_24_x86_64.whl", hash = "sha256:f04551dce5a7db8c9659f2e4245494c182d0663b83661803e08d46bfcae5eda1", size = 62339, upload-time = "2026-09-21T17:57:37.967Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c5/614d2eb9995232d066aae7f17d1d454060b6b5d882b22fbaf6150b05ef1d/multidict-6.9.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c0c88085affd35c33e124e36930c5ad96aff9294ce195eaa0fd9cec962b64a82", size = 55009, upload-time = "2026-09-21T17:57:39.746Z" }, + { url = "https://files.pythonhosted.org/packages/7f/24/f7179ecdf9cdd3581ace3292c3b883a1a500a374aa4e619bf6eb521c62e5/multidict-6.9.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a8b75dd3d3638d9a19f23e84af4ffab3b8940422c0df2da2a77005ef5aa3d7ea", size = 56273, upload-time = "2026-09-21T17:57:41.544Z" }, + { url = "https://files.pythonhosted.org/packages/ac/72/c9dffcecccd1f8b9a58f960ec4eaecab62c4edb08d5bb984d4de67fe3fdb/multidict-6.9.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:006c4478de0a1876f4834e14255776286f09b9846b505fe63f67f9d173a9487c", size = 96390, upload-time = "2026-09-21T17:57:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3f/76542360e655cecdb7c158c7af88c69c4db0d2aac3421659d41214927229/multidict-6.9.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2784090c30a586d5b45197bd9c32f87fb927216cde302f6cfd76d76577e90f08", size = 58900, upload-time = "2026-09-21T17:57:45.105Z" }, + { url = "https://files.pythonhosted.org/packages/04/15/f28c71af461dd4ff15653a165cfd8ddaf01f3387ff80d2dd0de057d527d0/multidict-6.9.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:7c708566da8014b120a64b1eb6d200c6c0c8cb36296383723cdb6fc82038270b", size = 56540, upload-time = "2026-09-21T17:57:46.913Z" }, + { url = "https://files.pythonhosted.org/packages/19/de/5ee4050c3ddad88c605a89fc1cd18e89abd40a64f183e6fb5276b6eb7f52/multidict-6.9.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:042fb0196047e786936a730bd302de83143950da45f2c16078da8f35e1cf7919", size = 331818, upload-time = "2026-09-21T17:57:48.825Z" }, + { url = "https://files.pythonhosted.org/packages/70/e2/2d4e3c3ee70f583334ff236fe6a3babf7a05ce56169a15df65453af647c0/multidict-6.9.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2212a0c842c723d919ea4a22a9296cb6b244b386e4e6ea92adfc7fbf3095519", size = 329800, upload-time = "2026-09-21T17:57:50.91Z" }, + { url = "https://files.pythonhosted.org/packages/d6/14/f64bd05667343a7250cdd99f81bda2270e3959f00832441f10c7f904e1c1/multidict-6.9.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd882aa29bf402b62bf1fd7c19fd5df4b6528cf468a908864b39368572b662a9", size = 312048, upload-time = "2026-09-21T17:57:53.053Z" }, + { url = "https://files.pythonhosted.org/packages/64/bf/a91256ce00199e7544e3eda79c4e9544eb7388c38fa22f11638c2e357ccb/multidict-6.9.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c57541034d12b215ab0a2bfa371d1a8a198da18176d0426c27105b9161a6862d", size = 343676, upload-time = "2026-09-21T17:57:55.069Z" }, + { url = "https://files.pythonhosted.org/packages/14/48/628b978413518159228c2b2ec38645362ceb6427e935d83d716e26490a49/multidict-6.9.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d73fed4e37158ff00cd271871170b79e40138db51e625fd352fa17c6acb34f67", size = 347028, upload-time = "2026-09-21T17:57:57.157Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/5f9dc2b5ac7dd7a82458ae9cd283b99c185834f23442ace295362dcc5e45/multidict-6.9.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:79da2491348b30810728050b4a8ec0416f85884125c2fd44655b3d01150d9c3e", size = 335670, upload-time = "2026-09-21T17:57:59.126Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/6f319ee000f5ab3fa0055b194ad706fddd552c60c76a21d581c87db1d46c/multidict-6.9.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b4da208a63434d21a3df64d29758e650fc4aa8cb05848554b76949c296539cca", size = 307804, upload-time = "2026-09-21T17:58:01.2Z" }, + { url = "https://files.pythonhosted.org/packages/3f/24/61be5fe1616d033532e8d63dc789c0f35b9793917834713eefe050589ba9/multidict-6.9.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:877ca17fdcfdf5c397493a71e5ff97a87bb181417fe717fdadc77c08c09301ac", size = 323409, upload-time = "2026-09-21T17:58:03.383Z" }, + { url = "https://files.pythonhosted.org/packages/22/a6/eab8aaf59892169a0f4e8e78a5516a6caa6abd8e96e6410111258f4ef0ef/multidict-6.9.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:40aec5299e1ed71fbb988389059da381c3c1a60e0c649acceb2a35d9b128848e", size = 315746, upload-time = "2026-09-21T17:58:05.372Z" }, + { url = "https://files.pythonhosted.org/packages/42/b1/d1e14bab80a74babf6e2ac971a878b84fdcf3a972f3ec3400ca12260fac5/multidict-6.9.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:4a409ccc42aefec904038695d5d7fd6d8f3af2721b7b6401d55135ec7d6d298e", size = 334529, upload-time = "2026-09-21T17:58:07.35Z" }, + { url = "https://files.pythonhosted.org/packages/f5/50/a05b1d36f6d3363a5a334cebbf11624c9c7a69d66fd23345eb53b1d43b7e/multidict-6.9.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:0a5769559e3312dd96731fbe15b4abb6033368ac1cad5a98dadd21946a4c7d6c", size = 336455, upload-time = "2026-09-21T17:58:09.112Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0d/bf33cb097380252b08e1177b5ae0cab822c47ce80d236f3f724953c76721/multidict-6.9.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:03fac50ddfd8302175b77863a015eccfae76767cda5506eef86df559ba861e1f", size = 305865, upload-time = "2026-09-21T17:58:10.921Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ee/a2bd133391204b1cec5a5caf327dc9a06710439b7403220860a8fb164560/multidict-6.9.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:5ecace251ccfa705bf3d7d35c5032cf750f5c629809740405697bce5c118c4a4", size = 337531, upload-time = "2026-09-21T17:58:12.846Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/c0972486f81a06c3bbb23063c85afaac53c51608f7e35bcb101a1006408b/multidict-6.9.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:ecc89dbd4155b2f8a47f4bbd89242a35ed15e8e0ec581cab5ac65fa38407329d", size = 332464, upload-time = "2026-09-21T17:58:14.673Z" }, + { url = "https://files.pythonhosted.org/packages/69/4f/5cbbe852b45d8a7f0ab3ed042386e4894e463010eb230603596cb15d0fee/multidict-6.9.1-cp315-cp315-win32.whl", hash = "sha256:a3ffe881246d28a862f1985824f484cb7361f44d6b99c4c620436ef56462f38d", size = 51714, upload-time = "2026-09-21T17:58:16.409Z" }, + { url = "https://files.pythonhosted.org/packages/d4/77/1fa118386deeae4e7a48870fa5cb073e748b0ac5265a6990651cefc48068/multidict-6.9.1-cp315-cp315-win_amd64.whl", hash = "sha256:59c123d0e948d760a5f930f316cfefa07e8d632ab84327c0693ee6a88171154f", size = 58619, upload-time = "2026-09-21T17:58:18.369Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ee/51c45748acfce9cb69d89dcfcd09c874b385407fd6993e885d6905659865/multidict-6.9.1-cp315-cp315-win_arm64.whl", hash = "sha256:31199204b3ced121ff5407a2c342326a5d27e3870abbf94bd80dbd2451b7bc8f", size = 53969, upload-time = "2026-09-21T17:58:20.102Z" }, + { url = "https://files.pythonhosted.org/packages/aa/57/89d2d3dedfae2558853c1770c7cbcdfd5211eb4c4090b1e8cb570544a490/multidict-6.9.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:8879510a76940670517ea1cb589978da44b86e286ec3e50d664ef330817afce7", size = 107679, upload-time = "2026-09-21T17:58:21.98Z" }, + { url = "https://files.pythonhosted.org/packages/c8/df/0c2a28870181c1762f90952b897f6db46c934ce073efe0442604cbc276db/multidict-6.9.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:aec65b53a07f580606593f877eefbb29a45939bfc0d3fe6e6d9f42b41b749f68", size = 64753, upload-time = "2026-09-21T17:58:23.622Z" }, + { url = "https://files.pythonhosted.org/packages/43/7e/9c6a3e7619459407d8958f7e56ab3dda7f0738f5aed9bb4ad48076e8de83/multidict-6.9.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:14c56f73e78faa1f68bbb826197cd5871994e70e841b8590829c35912ece5c64", size = 62732, upload-time = "2026-09-21T17:58:25.46Z" }, + { url = "https://files.pythonhosted.org/packages/38/eb/fd56c9d83ba0cdc3cf66f9acb278b24ef1f043b620b96d145685a3b57338/multidict-6.9.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:38c9986f9ce50c459b10de216a05f4bd7ed5ac63887d56e500a52bb464b861ce", size = 288648, upload-time = "2026-09-21T17:58:27.115Z" }, + { url = "https://files.pythonhosted.org/packages/df/ef/51e79c2442b0f56ac4ac580108d713078b31297e028d468fc220971a305d/multidict-6.9.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15db6a102cbaf1949cf028ecf080aac76d20bcd29ad4e092574db6c6b7af78a5", size = 304919, upload-time = "2026-09-21T17:58:29.143Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e4/774700cc5739353fe88a7204ed30e78a9403d98d4bb2496cc91f096d2e71/multidict-6.9.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd82c4977196681a499bb6ca9e462afbc5c91c1c15b6a991dfbd72733fea4dd2", size = 289094, upload-time = "2026-09-21T17:58:30.938Z" }, + { url = "https://files.pythonhosted.org/packages/ca/41/0bec327bc3a8271825376b6d8a469d1f80e3ba1e2e730e019836e29663c3/multidict-6.9.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bb58ba73a3f96f9a3e46b1fab69929d7edbbddb3133ee74b5c3c54074a53c4f1", size = 312007, upload-time = "2026-09-21T17:58:32.964Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/9906bf5f1fe20e8e755affccc1a69ecd8aebb079ac65386e5ffc8eefbe75/multidict-6.9.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c6eecfab7ce4cd9487ff8ba936fe38cdfd68c04faf3d5c710363c6a8e695659", size = 314657, upload-time = "2026-09-21T17:58:35.285Z" }, + { url = "https://files.pythonhosted.org/packages/33/92/cf6d665a45663bbe216420fe632da10e9f4f842b07ce02bc3ddf8eb882b8/multidict-6.9.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c62e71e6289d78c0108d8aeb495f8bd3cad4bc1632fedddc9297ddf287ecc20", size = 301563, upload-time = "2026-09-21T17:58:37.472Z" }, + { url = "https://files.pythonhosted.org/packages/97/4c/9f1b7945526905d58b4b2d422e2ea81326dda71f9bdc9c32c8002639234a/multidict-6.9.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:539c2cd5fed0947c135cd7eabaaac55f48300dfa1de0f3ca4edb5efa6606f471", size = 278540, upload-time = "2026-09-21T17:58:39.551Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d9/133aa472fc1be181de29a0c2e1c4cd778280882f532b404395f646742e46/multidict-6.9.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3100d169ceb7bc8f05f89a6db11d1b21f119975fc26f29dc45a472e3569f0879", size = 294858, upload-time = "2026-09-21T17:58:41.791Z" }, + { url = "https://files.pythonhosted.org/packages/bd/38/21f2305dd20ac5038fa581b5d7e0d8dbbd70dc6b50231c22ec173dacddda/multidict-6.9.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:696477ad71385c4795e3b8e4cf10b0d2c28c2a1ca6a955e031cb1e62993e9ee3", size = 285461, upload-time = "2026-09-21T17:58:43.844Z" }, + { url = "https://files.pythonhosted.org/packages/ad/21/465a43c214d1d7e2d0c974e727d4b5935f8a0868838b5df9d0984c83c1dc/multidict-6.9.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f5844e7befc707367807586f550fa97e23dcfef0728f02b98c7bc498a961a5df", size = 288964, upload-time = "2026-09-21T17:58:46.16Z" }, + { url = "https://files.pythonhosted.org/packages/2d/58/72a3e8d56c1e05146d2d1841dbaacf8986375eded1e5a58d30eef191a580/multidict-6.9.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:abc7c2e4b47bfe6a9aea434d3fdebb9597ee636e914e92ba352f2068b9142f4c", size = 303924, upload-time = "2026-09-21T17:58:48.395Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b9/911084c04530a682a54536b4f738d3992a593b5ff6fea1d6a1f02f3acc6b/multidict-6.9.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7ab379f95caee071a37cbd8be86d4c65accc651d391f9f97748fef006f38769c", size = 278479, upload-time = "2026-09-21T17:58:50.494Z" }, + { url = "https://files.pythonhosted.org/packages/45/eb/9a333346c02c928a9e82534638bce85697c10c46b4901fcd70af19357356/multidict-6.9.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:557a4e1708df428ebe6c3081c83a273d275dad2446ce0d81fc648ac71afdb18d", size = 301650, upload-time = "2026-09-21T17:58:52.554Z" }, + { url = "https://files.pythonhosted.org/packages/67/2f/1d936c4e45c8199db6082b6836e9182e31aa393de0de652637790831ca4b/multidict-6.9.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:475d04d5192eba487a3e2f935976340baa24529046e9c1c9c7a3b7bf80445ae1", size = 298024, upload-time = "2026-09-21T17:58:55.03Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0c/ca62ae2882b89a4cfeb0bbca873a4afdb5fc32994c4f8a37a7cb52ede7a0/multidict-6.9.1-cp315-cp315t-win32.whl", hash = "sha256:10083a8a0f4e1b26b599889e90b9802504ce5d3f7722f925bbb7ca47dd22a7c1", size = 57810, upload-time = "2026-09-21T17:58:57.141Z" }, + { url = "https://files.pythonhosted.org/packages/02/8b/b2c2805c3eb6fa4cdf5512fb95ac6174dde71e34dd601c5be52f4df5dc82/multidict-6.9.1-cp315-cp315t-win_amd64.whl", hash = "sha256:e96ca64383efa107262ee3949f047af5ee4f1845ba09463466c04d35a83bd3ec", size = 64984, upload-time = "2026-09-21T17:58:59.446Z" }, + { url = "https://files.pythonhosted.org/packages/fd/34/03f1d204d698c408f4754c969882609b6b7a902c0e3d16a7033f219cdeda/multidict-6.9.1-cp315-cp315t-win_arm64.whl", hash = "sha256:501ed8b02a5990c67a91c732843609d43a6be1f7576fcdfc867331239f37fbd3", size = 58742, upload-time = "2026-09-21T17:59:01.726Z" }, + { url = "https://files.pythonhosted.org/packages/be/59/e26cb779be4c591d1a910f59d29aca9fba4de70349840a833beba2652371/multidict-6.9.1-py3-none-any.whl", hash = "sha256:7bf6478188f4e47bf5686e8a33da4ae28bf43b1b2528d9ee144d28492bfac60b", size = 19176, upload-time = "2026-09-21T17:59:03.501Z" }, +] + +[[package]] +name = "mypy" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/4e/64300736cf0a0373a27b94a91b664ee7382e36f77b0621bae6381da3e180/mypy-2.4.0.tar.gz", hash = "sha256:77bdaebd452f43fcfc4cc3ba94352a3ea537cd01e3f2d0879f48673d2ec00d6e", size = 4064425, upload-time = "2026-10-01T20:40:39.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/0a/78c9ac93933e2ecd51d807021f78c03ffe98640f33951a8be159f5fd55bc/mypy-2.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffda5244fd1ad71a1e54405e35f50d09b80c3978efa120f58bd1252ae32c62d2", size = 14408618, upload-time = "2026-10-01T20:39:46.766Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4f/5f5f4429fd5a4218ce6268164a66afeed75c7e2ce5522a4d163faaa5fe85/mypy-2.4.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:615b03922d40e186fd1df73156473db0ba525c639bb4fd1cf28e1694879c9b23", size = 14552749, upload-time = "2026-10-01T20:40:08.325Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/fa1c319cd51db7a6027b46117460d5a361c6bba0c0f0f0670a2dab8ce2a5/mypy-2.4.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13fa24f439c0e48a290a3922fa14ccd22f2762bae99d2142931b3e40a9055080", size = 15505846, upload-time = "2026-10-01T20:39:15.475Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5a/14c02690776fb04e9fa60b81173cb9ae6315e0745fad3813b303737f2f0c/mypy-2.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3adef556a19eb3b630bf86a79c29d7da3d61e541472d0d897ca01b171c0abf8f", size = 15823059, upload-time = "2026-10-01T20:39:35.429Z" }, + { url = "https://files.pythonhosted.org/packages/76/d8/d29afa819f8cb765018d6e2773e390a41ea4592f75abe417135969646857/mypy-2.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:1dc0f64b0a92ae27a49d2175f0bacfa56e15bdc5b420cf92e0c1f79292219cbb", size = 11312927, upload-time = "2026-10-01T20:39:28.509Z" }, + { url = "https://files.pythonhosted.org/packages/bc/c8/966dec4d80303855d824e6bf67fe1da812d88adfe4d75e24d8545dab4826/mypy-2.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:4a378fc15fb33e321f04652c166ce73eeb8833a97c3d218132844e938cd93220", size = 10879252, upload-time = "2026-10-01T20:38:41.545Z" }, + { url = "https://files.pythonhosted.org/packages/26/67/e53965e67d23d48e78dd603aa7525a8d77e7818c23954cc217ccde5cd5c0/mypy-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d20e6c7c35fcbf2a0ebdd0eaeacfbc243009dfd33ab7822d54e213912e6dbbd", size = 14315745, upload-time = "2026-10-01T20:39:25.03Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5d/a588ccd57625822d8484d7aff4e9c70f88d042a91c2ec84324a2bc14b3f3/mypy-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3011537be6cf1de4511c0255a324362a812b58184bbe61e15f59c8b31033bd74", size = 14465652, upload-time = "2026-10-01T20:38:57.19Z" }, + { url = "https://files.pythonhosted.org/packages/05/af/b4978a5566cb829ca563dc08e3ad8d944f116078786ad54ba3dff2bcb64d/mypy-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a96b07a49b7b1d025ce59c1b3acbcf24bead9a83da4523c4a6bde1bb94e7a0e1", size = 15419180, upload-time = "2026-10-01T20:40:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8c/52c71f31c54d0a749d832cc73b933c6e38c52d9b270d5503e031c73723dc/mypy-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7c4f8f8d1d1c0e2832d8ee7113dd08f6df6c7aad9e863fcbed9f25832be0b8c4", size = 15669212, upload-time = "2026-10-01T20:39:20.443Z" }, + { url = "https://files.pythonhosted.org/packages/e8/55/ba63121494404fd7d3787ec176ccd02820d6ec125ac4e7c4c0b79f2cc6c7/mypy-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba05652540bf12828e52abae807b024b09ca144ff4f75e2450a81d69c376425b", size = 11299572, upload-time = "2026-10-01T20:40:03.627Z" }, + { url = "https://files.pythonhosted.org/packages/9c/9c/385c81b5c0d3a2917f077c4da660d0493f9521ccfd4b058e33b860f72d02/mypy-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:6306086b87cf7f8a29aa618d9fd9bffb56c59247166b9660fdb54d86d7714ecd", size = 10869751, upload-time = "2026-10-01T20:40:25.483Z" }, + { url = "https://files.pythonhosted.org/packages/68/ed/e5d7cf4017e74a1c1e1c4058ce8f614fc1e3e7606564f47166e22bfc9f95/mypy-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e05ff2925d8b37ad26c80c1b9dc43ae5d455da2df1e23c24c095a6425917c57e", size = 14378593, upload-time = "2026-10-01T20:38:48.222Z" }, + { url = "https://files.pythonhosted.org/packages/30/7d/12d994886a922f0f1997becc9c6625198d61eeb5962558a886af7dd38d54/mypy-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29243242cf72582b65f9582ad9e56e8cb281566ed3519f4cd70bb8b9f2977e90", size = 14464697, upload-time = "2026-10-01T20:39:11.059Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9e/bcc9af755425ad17790bf11d73c2ea7592914cf309ee1340997670f9d57a/mypy-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29eb0b9427a6b11b992e452f6cceb8af724f4dceb47e779d0b35e405e996ea5e", size = 15552257, upload-time = "2026-10-01T20:39:56.269Z" }, + { url = "https://files.pythonhosted.org/packages/af/0c/3343fc4525d6f00d75ad17a93a9f052d5163641cb8911840d4a12cb59ff2/mypy-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e3ebe2f72a2a1156065a9851570ffbf50c0a93cdccadef9c6e05c508a4fd10b1", size = 15800443, upload-time = "2026-10-01T20:38:54.815Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0e/69aac6b8159da7c53e6115a2be1bf48e85503402cadbf506dd708e8b2ad7/mypy-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:236e0d68f6941992b0811128e652590f590db444ab29ad8f1324765b9298b946", size = 11381574, upload-time = "2026-10-01T20:39:49.406Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d3/d32ce4feb5993eec09d2024bef16cedc9b93701b1446b86092f08b24491b/mypy-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:82d0f94c8587ccb472622ee7795280aaa38a06640d5f45b3f16909d6dd86a989", size = 10944729, upload-time = "2026-10-01T20:40:15.203Z" }, + { url = "https://files.pythonhosted.org/packages/44/f2/eb15183c97c69d7cbfac990a6efd33a19ecfd97dab9e714c742fa78a784f/mypy-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7da85fbcff6dac1abcc636707bed38b45598131fb7a605d9719c70b5cc733af8", size = 14213395, upload-time = "2026-10-01T20:40:21.178Z" }, + { url = "https://files.pythonhosted.org/packages/8c/b5/ba91b6ff65e4d6b6ff53b2b3b3ac5f1babf0c7c27d0b43a0196b1c967926/mypy-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4209da39d85cf240f762af622d8180fcdfcb4727d021f44ade62d613a1a43324", size = 14373009, upload-time = "2026-10-01T20:39:58.86Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c4/484275efc935c0003e55e4e8a33e4b8e99528ee956c12256ece4708f903f/mypy-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be721bd4bd57576193653b75b4af3461c9d0bf7dd8b528f782e9be210dc75bb", size = 15465557, upload-time = "2026-10-01T20:39:01.794Z" }, + { url = "https://files.pythonhosted.org/packages/50/30/66eb6fdd0875e3c9025a02f0bb0ea2e524b74274b658c37fde0068c4939d/mypy-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1fde197ae65be856a034a91b70ed747a16562ca69577785f06c661548424bf1", size = 15728673, upload-time = "2026-10-01T20:38:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/58/bc/2aa98fd7f49c42dba8e9c065886fadffdd00f3c654cff3f2a7103a797ac8/mypy-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:295ecf2e57542cd836ca537486951289678c8c7d1ee6ad74ebe29b2168a003cf", size = 11389803, upload-time = "2026-10-01T20:39:37.547Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/17b60df2d946792ef6af43b89351f5c9ddabc69053206b2304945572a744/mypy-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc378bdad4e9f12b5bd96466083d1e71acf00594ec9c7b2bdb5e02816f77f303", size = 10951060, upload-time = "2026-10-01T20:39:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/e0/66/924be0b653372ed31ad5c48e26044cc00840e591943a56e61621cf05b60e/mypy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:058165f564ccf559c68c70fec2091fca5891110480210c22594635e3f6683437", size = 14234230, upload-time = "2026-10-01T20:39:44.535Z" }, + { url = "https://files.pythonhosted.org/packages/56/39/c4f176880a4177123576de6cec6309feea8f42fca2bf2f6584e88054f656/mypy-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fa247e02b505a45a2775f69df38d360d197e3790bc60f717595db9eda358b6e", size = 14392944, upload-time = "2026-10-01T20:40:05.78Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1c/26e16977e25ef2494a74f8ffc872a76ec2c3aa43c3156057cbdf88f352c5/mypy-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20e9a5cd875837520c43db98dea0b6d0c2197833d95c30127d8f570fb9b1f00b", size = 15441463, upload-time = "2026-10-01T20:40:32.412Z" }, + { url = "https://files.pythonhosted.org/packages/31/9c/9e4b049f0ecefbfd6817ca2a16eea55dd74a07950268edc6cffd29ccfe98/mypy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9f03a7828cca2b0adcd6662aee8f2711ff8830e1027641fdea3ab0b787483566", size = 15705406, upload-time = "2026-10-01T20:40:34.844Z" }, + { url = "https://files.pythonhosted.org/packages/4a/5e/e861b5f6c5ef9ee6cd24683aed1edecf82a26dbd536049f9b7850b586267/mypy-2.4.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9279488933040b638c0ab739084c0ca100efeea6db581bf5d7628d8e89de53fe", size = 7897523, upload-time = "2026-10-01T20:39:39.861Z" }, + { url = "https://files.pythonhosted.org/packages/3a/87/61ffee58b25a956532a6006fae84918a2de849ed25f397492b2e815fe7b3/mypy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:2106b55105ba5ea9be4f53a24517fc5fa927ff1585edc9bc1a975abb72caef89", size = 11576345, upload-time = "2026-10-01T20:39:41.553Z" }, + { url = "https://files.pythonhosted.org/packages/a1/88/a331c20698971c2ce8d1c30f317fd61b5be13a85b1f053e12dfa22ac6568/mypy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:528c8744b8b5e3ecb8774f86af38d2376216816e9908317ad055f3c9c2d74799", size = 11172062, upload-time = "2026-10-01T20:38:59.534Z" }, + { url = "https://files.pythonhosted.org/packages/47/c0/4f7daa73270dced8e86c6f4a911c68082d03a84e6c1ec9bd916028d66131/mypy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0bb95cf34899e4619c61ab0a8667804e139e580b30d5df12af2102dfe44d0c97", size = 15711208, upload-time = "2026-10-01T20:39:30.597Z" }, + { url = "https://files.pythonhosted.org/packages/2f/05/f1afa303c678be24cf7a266d38fb24b3de4599a024c2f9ba0d5905a3efa3/mypy-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d616fe84c6eab8026f8c50ab5bcb90db780d2ccd233d971e34e92bede9b359", size = 15935442, upload-time = "2026-10-01T20:39:22.781Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d6/6a1a45459b63716e0d035f4892a926d3054f0a8cbfa02551d228a9946083/mypy-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3f86fd1313dd69d013e265f1fdcd12ea7a9d606f9875b2a3db946cd334555f3", size = 17240044, upload-time = "2026-10-01T20:39:32.95Z" }, + { url = "https://files.pythonhosted.org/packages/87/85/ae33bee66c13f98d421964d87bf0888be941063bc75c1204a4cf142cf1cc/mypy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:720434d48542ecfe84d32d287b727569d3fc8f5769acd39051130e490a5c295c", size = 17545856, upload-time = "2026-10-01T20:39:08.827Z" }, + { url = "https://files.pythonhosted.org/packages/df/de/eeff209b65c3e267818d79333bb9f058da6e4e73761461bd94748a2eb628/mypy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a6e851b82c0661f69f1630fc16172c68787a6a9cf0991e7c6437d60976cdcd76", size = 12369342, upload-time = "2026-10-01T20:40:10.516Z" }, + { url = "https://files.pythonhosted.org/packages/2b/43/e62d8d5c1dd737aa248968302ff7d6eabf997c3301773ed8bcb64932ca77/mypy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:3bd0e340f0ebe65c548210f53be3fd8192e83964760caf0c28bef368e68b0d37", size = 11849099, upload-time = "2026-10-01T20:39:51.834Z" }, + { url = "https://files.pythonhosted.org/packages/2e/5f/335b8980055118dc131355155883fb676bb2161a0d97e08658e479c776fb/mypy-2.4.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:afa89837d9be67e0cadfa33bca3bb7efdda98c3b07e74dc3b635ebfb1c8a926a", size = 14241302, upload-time = "2026-10-01T20:39:13.227Z" }, + { url = "https://files.pythonhosted.org/packages/18/37/1482fdc49332b145828912b15f16eee0c4ca70a8bce0f6514ece79b14680/mypy-2.4.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb443e81057896132d3642d6be219e6efd158691ac7883e3ba8fcb469865f05d", size = 14408200, upload-time = "2026-10-01T20:38:52.576Z" }, + { url = "https://files.pythonhosted.org/packages/04/09/dce2e8f6c1b31053c430ef6963f6f7a38ccb49b90c5e01e37ed0129b7d5a/mypy-2.4.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7f38f57d344f8b6accb40e01c3d83cfc590498231724d16c07ffb7940f157818", size = 15444409, upload-time = "2026-10-01T20:39:18.1Z" }, + { url = "https://files.pythonhosted.org/packages/43/c5/91b68306da4cd280cb15be35dbb5ffc343cabd67c4b121b6625bf2d13177/mypy-2.4.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e76172710bd4e5eeae061abfd68347e5264632e02778be61784671ae3a2132f5", size = 15731186, upload-time = "2026-10-01T20:40:23.426Z" }, + { url = "https://files.pythonhosted.org/packages/64/71/2d0340182a8f27352fb1e25531355951108b506097c433937e9eec452ffd/mypy-2.4.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f83353e47ab520bf6fd4df8f5897d9fe081211f2fbc4b7d37736a3e3c166cbcf", size = 7897838, upload-time = "2026-10-01T20:38:50.779Z" }, + { url = "https://files.pythonhosted.org/packages/3e/08/32703c117e134c02efa2a82705bb40cee91eaedea10b6530e20eba651b1f/mypy-2.4.0-cp315-cp315-win_amd64.whl", hash = "sha256:970b221ed5842213d98e3c480c08f795ace4b1f81fb21e1b126bd0476bce1c34", size = 11575700, upload-time = "2026-10-01T20:39:53.982Z" }, + { url = "https://files.pythonhosted.org/packages/ab/08/08bb269feafdaad031046ee2d771528a64467e6a180f962afc28c4a3ccd8/mypy-2.4.0-cp315-cp315-win_arm64.whl", hash = "sha256:502b94b0b331f7dafe32fd6b151797ddbb4f32385b362e722c783a025e5954a3", size = 11172908, upload-time = "2026-10-01T20:40:12.91Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3f/c5c92626006ca92c7686adfbece47fc6a0daa0e54753952a9ad13ce0561c/mypy-2.4.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c9de622fd397495695d0598ddc789222bfcfec9d7c9ec3a1e385c855e3bc5e01", size = 15705970, upload-time = "2026-10-01T20:40:17.79Z" }, + { url = "https://files.pythonhosted.org/packages/10/f3/863365f7997a76a5a1dd42d7902ab05afa4124e8419089cbca1ce2554db1/mypy-2.4.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f459f0b4f0596d9d51fe7716b404b35287b99e77da98a7af90a65dd5fd61141", size = 15942234, upload-time = "2026-10-01T20:40:37.243Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/2f45b1f425a2c95dbe1a3f4d076bfd42b76e9615dba5906230703b14e4eb/mypy-2.4.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9b028548b3af480e2b1ed8df14ccaac86f99c9f600d1580770ab7ba3dcd40f0", size = 17218364, upload-time = "2026-10-01T20:39:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/43/8b/5b2bbfc69e84800b78fa2dba16d995b003f93b573558e412c5490d6e6c37/mypy-2.4.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:cb734b2668c1f40d07ce093bbeb4407e9527c67901627b0e1679825be3f09975", size = 17557934, upload-time = "2026-10-01T20:38:46.077Z" }, + { url = "https://files.pythonhosted.org/packages/df/c0/1a5dc601c22041a7bbfe1ff71cf097fbbd4fb49930867c6789baf5102106/mypy-2.4.0-cp315-cp315t-win_amd64.whl", hash = "sha256:172e30b8fea631fe310f0c665477f52d9ea40bb4e99e0c81dc30118563b13710", size = 12350955, upload-time = "2026-10-01T20:40:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/1c/bc/697e9e26fc2a86c094ad67ee1e419971b7f01ded66ee66231b09e9f3e12e/mypy-2.4.0-cp315-cp315t-win_arm64.whl", hash = "sha256:5786ef987b3767e51aaa53f20aec104c0252b42ecda7aef8e8b4cbae279b05c5", size = 11842738, upload-time = "2026-10-01T20:40:30.026Z" }, + { url = "https://files.pythonhosted.org/packages/81/12/46ae8670c98a3cd0286ca5645c2f918f8f6be65edfed81b916010619f668/mypy-2.4.0-py3-none-any.whl", hash = "sha256:d01c5d26a352acc6d5cf3128225477e1e8465e8d3029d4c345807fbf7f3cf093", size = 2800734, upload-time = "2026-10-01T20:39:26.837Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "propcache" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/9a/9fbf4e4ec0c2d7f1c32519fff782ef467859b8faa9fbc5331a96f6395d43/propcache-0.5.4.tar.gz", hash = "sha256:ff6b113f50bc066a698db5d944d2c6dc7507168dd3341e255a8892fd0715a558", size = 61545, upload-time = "2026-09-16T00:17:14.386Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/dd/997704118aea215cc5f65c277f5323657b4ba44c1f9a32fb11c8064e4997/propcache-0.5.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b77c313314524ca9c38fbd70f73515d04597ac58c40c939bc0e71eeb4abff680", size = 87225, upload-time = "2026-09-16T00:13:43.864Z" }, + { url = "https://files.pythonhosted.org/packages/3e/6d/ceeca1762ed51230c08d557591404f844ecef5e294550136155572f7faae/propcache-0.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8f911c395cef73c510bac566da9507bb6a43e7763d0c79138dc60ee53f11207e", size = 50809, upload-time = "2026-09-16T00:13:45.33Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9a/6be90814d8762952594a9e380161802541bff690f3a2e011dcc28ec193ce/propcache-0.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d83b12902eb8bce151259c86c03ba746600b2d994543de46e370cecf96c452f2", size = 52552, upload-time = "2026-09-16T00:13:46.48Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d1/7fd3ffb5ca0e669a8fbf55d9fbb17c50ff5f44becfefcab27a9b9b67908f/propcache-0.5.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9281e922c072158c91974d4589f1dbe0fee6d467f284c28e463f9f5a4d933f4", size = 226804, upload-time = "2026-09-16T00:13:47.739Z" }, + { url = "https://files.pythonhosted.org/packages/74/87/a3e199c45b26587f073db655d19c30d2144b0f883827ba6ab77acf5c7d45/propcache-0.5.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9f3551b8a35c1df3e7ea4d2d86edee15f0dde1bddd434a71744048683544d0ef", size = 234452, upload-time = "2026-09-16T00:13:48.98Z" }, + { url = "https://files.pythonhosted.org/packages/12/7c/08c15c7df256f94a6b4563f74165c3242fb554cdd1909b661d093c31b976/propcache-0.5.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ec6a85f424afa8d23e0d9a094e5dbb6eda01da91c92b9183cd433768247ffc97", size = 240399, upload-time = "2026-09-16T00:13:50.416Z" }, + { url = "https://files.pythonhosted.org/packages/f0/51/5adee15e12a7e314cece4543fbf295b0fe1b504dc78b32dff4c8eb1e29b0/propcache-0.5.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f574e460d1c8a08384a016fdb09ccf3543433263ed6b2f97104f979e64ea57c2", size = 224251, upload-time = "2026-09-16T00:13:51.874Z" }, + { url = "https://files.pythonhosted.org/packages/9d/53/54bd510bb5d473edf66914602885226187218b4e3a5017e9dd80bceea41f/propcache-0.5.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8e017eeb7482bed34cdb0d61cf2bcfc88d104bbab296a17cd16a6af8aabc70e", size = 202721, upload-time = "2026-09-16T00:13:53.524Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b4/a468700d0526dfb2ca6af5d790125de5894acc7d8ecd99243ecedfa4c4cc/propcache-0.5.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f273dcf7149a50527c4fd1f55cfe9eac0f60753f5af544b4c9352578e20c0874", size = 219148, upload-time = "2026-09-16T00:13:55.068Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d4/bf563e19ac9a5cc47113431337fdf2ee3573859f3f3e0a58e59833e9b75d/propcache-0.5.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:fc2461ecc45f17893f8207e73b46ea8ba93e33630e51cf4af3fbc21d47462b1a", size = 211011, upload-time = "2026-09-16T00:13:56.588Z" }, + { url = "https://files.pythonhosted.org/packages/4b/3d/0189b6537f4a8cd795e685a130a6f47aeda78a5b7b062574705cffc685ca/propcache-0.5.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:279655a16973f1ee2bd2fe79973137681642fd9ae0d89215bba263726eb0dc3a", size = 228148, upload-time = "2026-09-16T00:13:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b7/59c16e245a549df202b4ebe2de91fa58a67dc4373df9840e372a64224dee/propcache-0.5.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e9f165403b81fea7e89c932d89046a1e3d9a3a60e8d7ef2f249dccdcb0982bf5", size = 201432, upload-time = "2026-09-16T00:13:59.453Z" }, + { url = "https://files.pythonhosted.org/packages/41/0b/7b19eb20bb0b1f9476d08f6e387ae094dc6870fc06d459ea1f35f28b25d3/propcache-0.5.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1783582065a1f07f9d9ee1e992e13f15d7dc8fb1eb3a7476d43eb3f2e69d26bb", size = 228923, upload-time = "2026-09-16T00:14:00.731Z" }, + { url = "https://files.pythonhosted.org/packages/3a/f9/b1a0bd47218216b935d019912d6fec1676ae7c07f15fa82bfb54b8d58976/propcache-0.5.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d605bb239b796e82a81c6709548b2bd460ab73b4590cb0c83de8a2dd9694d0f", size = 218327, upload-time = "2026-09-16T00:14:02.091Z" }, + { url = "https://files.pythonhosted.org/packages/55/c0/51e5d1ad504e9529831606534c48e272db751072eee9ff07e4abe37e50bd/propcache-0.5.4-cp310-cp310-win32.whl", hash = "sha256:141fdbd73748db0cf7636035030aaac383d2efde8f34e7bc24594cc776d225b8", size = 43096, upload-time = "2026-09-16T00:14:03.427Z" }, + { url = "https://files.pythonhosted.org/packages/80/57/0a0b4b1122f4ed5bcd1c626d910003cdf5282c2a12f8a1cfa17970b3ded2/propcache-0.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:146f48a9e4812611a7581003b1a39de56c34967046310c4171a68ef908c9a745", size = 46589, upload-time = "2026-09-16T00:14:04.581Z" }, + { url = "https://files.pythonhosted.org/packages/66/87/e71b24adc8ece61782ba3d6f3879e6a07fdb85bce21a9c529524184e3ec6/propcache-0.5.4-cp310-cp310-win_arm64.whl", hash = "sha256:6c7599df2b57ebeea8de011b5f2f7b85de95e76037d43d34b95e328430275487", size = 43982, upload-time = "2026-09-16T00:14:05.728Z" }, + { url = "https://files.pythonhosted.org/packages/1e/40/14b21e505b7921617466576423f188a5c9caddfdaa1cf4b2b8a83d8fe216/propcache-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:897d1ddf6716e8f47200f7aad9a0efa6cc7586df66c6defa572f9eab379c078e", size = 86393, upload-time = "2026-09-16T00:14:06.9Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4b/5a52e1a7b43563f7d408814194bb23cc8bf214eb6b86639b667a33a8d0d0/propcache-0.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9cbfff4423eef4cc6cafc021469641a2b835f610b2647a6c5281903e21b8670d", size = 50431, upload-time = "2026-09-16T00:14:08.025Z" }, + { url = "https://files.pythonhosted.org/packages/05/cf/b5248180bf056cc76acc60c9c6e8c0ebbfdbd1c6cffd31fd14996927b7c8/propcache-0.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fc24f209c1b7f7f688b66b98293954f5504279760999b58920ee12dd8471c1d", size = 52116, upload-time = "2026-09-16T00:14:09.114Z" }, + { url = "https://files.pythonhosted.org/packages/86/a8/7c6cd6bfead1a11f2e411e688640e6d26574cb0bde7dcaa7423b0b65ed7a/propcache-0.5.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62530ca89187827e4a4fe733f971abe81a7542eeea48ff61995f19b64d7199c8", size = 238729, upload-time = "2026-09-16T00:14:10.357Z" }, + { url = "https://files.pythonhosted.org/packages/5c/b4/442715b2e980df51be52d203549279e027728f24c80b00b5e525e31cd5ea/propcache-0.5.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fc3f7599528db40b1efa0889a620116e2704144495273d66066e8164e45838", size = 246121, upload-time = "2026-09-16T00:14:11.735Z" }, + { url = "https://files.pythonhosted.org/packages/bc/5d/df0684fc2b1732a01a7bec26d7897369022712422d25b09c37ce7dbc88a2/propcache-0.5.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f2d880ff60f45898f4acfa152aac8d04e3ee627d90ff4003491bf92239d5757", size = 251735, upload-time = "2026-09-16T00:14:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/c7/06/519a5ebb48b6f94beb48396e55c905f12246a25c3a3608a7ec7bceabf50e/propcache-0.5.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e9368e87a3efc285e559131092c5db643eb8e56de4ee42064d5baec22ef2bb5", size = 235381, upload-time = "2026-09-16T00:14:14.398Z" }, + { url = "https://files.pythonhosted.org/packages/3c/07/1e0a9bb310830f2245edbd5cd3c6d24a783c053c4efd8e08e386e513c940/propcache-0.5.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:004e685b315646c410771836e72a44f143bbe624f29653a42687815069a303d5", size = 208973, upload-time = "2026-09-16T00:14:15.715Z" }, + { url = "https://files.pythonhosted.org/packages/62/5c/9324fab27d6088eecc47fe4332bf7aaf8c1ded93c36f558391e8a06d41a7/propcache-0.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:594eb4c6ec35e7179b058481f4e9f02521b56de16fa577c4b85c76fb1bf8a9f8", size = 233897, upload-time = "2026-09-16T00:14:17.25Z" }, + { url = "https://files.pythonhosted.org/packages/9c/a3/570d92fc952eae93b676f3a1568f4b89264102abd3c982ab6a9ebec58dcf/propcache-0.5.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2dba2f02d2d5c09ef8a0e6c1a42aeaa451f4be9898cb00b04fe98717da2eb23b", size = 223512, upload-time = "2026-09-16T00:14:18.87Z" }, + { url = "https://files.pythonhosted.org/packages/65/47/26810d889d89bba31db397e6a88f8984af775f5ed6bad0a29dce84324cff/propcache-0.5.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c3ef2818d63bc86071e9d2989ae75a1bc32b8f7059cfd9f5abbbee70c32e2ed6", size = 239043, upload-time = "2026-09-16T00:14:20.366Z" }, + { url = "https://files.pythonhosted.org/packages/89/2d/f9c47691aa024c8299a3afacd78d22a01ab57eb627b481b6089708e71017/propcache-0.5.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:dd2ac8f5b643454c2cc6b6118b13da16e88f4a6434fc3ba61aca384029f04f36", size = 208218, upload-time = "2026-09-16T00:14:21.801Z" }, + { url = "https://files.pythonhosted.org/packages/74/6b/d510c0c378cabbf9d0ac7b663af6d00f2e9074073d93b20c85f24aa5c071/propcache-0.5.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4054acf80d40456a0537f2913b349718649d8d6458a14ab7f48d0ce28c30869d", size = 240301, upload-time = "2026-09-16T00:14:23.121Z" }, + { url = "https://files.pythonhosted.org/packages/3d/80/c80f6adaaa1e51f0db2dce8c9b3714d94ec45e358a21f9a1910b10b40a80/propcache-0.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40e94adb1e7d39ff28a8bd8d8b8fbd1df6b9f40976dbe379134f1ce058e532dd", size = 230785, upload-time = "2026-09-16T00:14:24.458Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e2/c32a7df3f39caa7f11b2eb37ea5b6960a6946f2bc7c4b8ff97bbdf6d6b6e/propcache-0.5.4-cp311-cp311-win32.whl", hash = "sha256:9f86f7259efe2c951f43e57d471c9b41daa5bfc7db9f67189059cf1ae6d77fd9", size = 42747, upload-time = "2026-09-16T00:14:25.715Z" }, + { url = "https://files.pythonhosted.org/packages/0a/8a/3db6a3543d8101263b4c52978b6276a04ead2caff2c5ab880d934f47bd89/propcache-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:e904d4d01f36bd6e197590be1533c44e06058771e0746dd073a8ebb3ef880858", size = 46268, upload-time = "2026-09-16T00:14:26.996Z" }, + { url = "https://files.pythonhosted.org/packages/41/07/5222e2665bbf6e45847492ecbf3b9f3e4975a0ae300e5fd465df7d48ce55/propcache-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:d42a9a856a4a6e2f6c10f1318c07e7daa498d6593abe745c71dae4521a26ca39", size = 43547, upload-time = "2026-09-16T00:14:28.143Z" }, + { url = "https://files.pythonhosted.org/packages/71/cd/348d58f142aebc4873345c6b31087629182ca6e0f2b3caeaa528cf882eba/propcache-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b28f41fa3b8c6900457f858ec5b03998f3a6d535fbc1bb2edec5961ea05ec429", size = 87285, upload-time = "2026-09-16T00:14:29.362Z" }, + { url = "https://files.pythonhosted.org/packages/df/f4/f3ffaee281b276da854ac1d7a6a506d26cbc62ea2e623756f1d0a4a1ba1a/propcache-0.5.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dcbf346a318a5e30063f547630b02bb787ce2f45b6368d5da143660b6a3835d8", size = 50984, upload-time = "2026-09-16T00:14:30.473Z" }, + { url = "https://files.pythonhosted.org/packages/25/88/1d7df7201750b37765ef2b23bc1c526c028dadde80afa0f57a118fc01182/propcache-0.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87a3caecf8095e48dc72f84bfa42e23a848cf410cc9cc13031fba4869b706a21", size = 52460, upload-time = "2026-09-16T00:14:31.692Z" }, + { url = "https://files.pythonhosted.org/packages/83/4f/48865bd02a16ee5236bc46166b2946f37b93e07b0eae355dac0be0b216ca/propcache-0.5.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60a64cbccaa11b7760ce705a14ada17ba459e7ca9f23ba587eb013821032d7ef", size = 251768, upload-time = "2026-09-16T00:14:32.908Z" }, + { url = "https://files.pythonhosted.org/packages/b0/19/3742a5eed62317b03b4002ee865dc9fd720308bdd0da1f29a5786c630311/propcache-0.5.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a74bfa37147cc08fb29df10bd9c16f40fa7f860cd3a6d2fff853323a94f6e17f", size = 257723, upload-time = "2026-09-16T00:14:34.267Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/ee6350fb0be9122bb6c67082a876d34b90d980d100c106af4b81023e04f4/propcache-0.5.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a4d7a54719b67338a305dca2ce6aafe366817df94ddfd4b5514374356f5ca546", size = 265597, upload-time = "2026-09-16T00:14:35.56Z" }, + { url = "https://files.pythonhosted.org/packages/85/9f/83a07b6ec0e043c050cfdd35fb0cf1b7897b91d554d6eea293740309afe7/propcache-0.5.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2814ecd8e818f487bee4b0f921bc4d1c176cc5fc71ac0f072d0fa67eda4ac14b", size = 250424, upload-time = "2026-09-16T00:14:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/33/2c/a763a8251f50fba042af0fb1f02bfec4b31381e40aff760db2be7b2e1f84/propcache-0.5.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6af4693716bfb03f1752ef1b30faa593db2c01d5272e9b8564a1549452a979ab", size = 216748, upload-time = "2026-09-16T00:14:38.369Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e2/4d11bea8fd6a777149c6c20645f873952eab5de3a2497aa11648ec9ab6ab/propcache-0.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4fbc1a15dc8cd1689508758d626b372b1f09d28d9577667feaf9e6bfcd8efcbc", size = 246533, upload-time = "2026-09-16T00:14:39.82Z" }, + { url = "https://files.pythonhosted.org/packages/9f/36/6683597de4907e70c717e3588c541202c66086a72ff3db58be49de66e72c/propcache-0.5.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cdee8205a44d0be91bbac4c41b95d86641b72dfc7aef1279400e4fda3f26a937", size = 238173, upload-time = "2026-09-16T00:14:41.259Z" }, + { url = "https://files.pythonhosted.org/packages/85/84/cb08d79f1762daafeb2b030c470cd0c725c97b8ad67412457c6f35c53e9d/propcache-0.5.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9a2a8a50a93dee0268a860a07fa3b4bd968f8ce4dbd794957da772f395368526", size = 251128, upload-time = "2026-09-16T00:14:42.652Z" }, + { url = "https://files.pythonhosted.org/packages/c2/0d/41b848036db6621370c1f2e5471a7da8149c730f8552a5257567721f4576/propcache-0.5.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7ffafcbfc7b549ab940047e505c831eabac5e67de53e1bc174adbc5285c55944", size = 214821, upload-time = "2026-09-16T00:14:44.112Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/adfae4bf9c63bccf12e2d9690a175c6579047a6eec3b5a6a5f51428c15e2/propcache-0.5.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d1f5a500bfcbb2c0ab85e98a0dcd70f5899d34efe365a0187700369a79603031", size = 254793, upload-time = "2026-09-16T00:14:45.429Z" }, + { url = "https://files.pythonhosted.org/packages/51/6f/eeca9647245d5f92e87d53e5f14335bb42fce1a7e6842c8045b364eded8b/propcache-0.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a235f73d6e020855dc29dff012d920c02ee0feab8d73a24185a7569f4be1161", size = 247134, upload-time = "2026-09-16T00:14:46.976Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a9/424e38838793d37160b4379c702f61c74c598fc6cd17204adbe3c554f7a8/propcache-0.5.4-cp312-cp312-win32.whl", hash = "sha256:b3083bfe87f95c756e610bd8025f26cbd1cd4aaa03a422f2d65efb7a97cd53d8", size = 43073, upload-time = "2026-09-16T00:14:48.338Z" }, + { url = "https://files.pythonhosted.org/packages/58/7b/6e8ef26f6d510a7916064fec68d55fcbfbdf7eb01e377480d66a122152d8/propcache-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:98914de2c4d7f0f9f4a8c6ea4bf05841f4175796941e3ef7d47eb718f22311fb", size = 46190, upload-time = "2026-09-16T00:14:49.99Z" }, + { url = "https://files.pythonhosted.org/packages/08/b9/72028c5b56ced97f456de6aefa79435ca64d7f77af78ea8cf3c76fc5195f/propcache-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:8876b39961e33d912afe3c1bee18ee564fdad0206f873cc15d522756b7f50737", size = 43075, upload-time = "2026-09-16T00:14:51.155Z" }, + { url = "https://files.pythonhosted.org/packages/78/4c/3b1365d58a667689e067e13d055fcd92bdf8d9a2fca3d9201b47ed5b3631/propcache-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:36c0d9db44b523ef93d03341b1c42d69ff01d673c053d1b1c6c3a363bcaa39ba", size = 85290, upload-time = "2026-09-16T00:14:52.342Z" }, + { url = "https://files.pythonhosted.org/packages/8f/61/5f9c29c3aa67c30238c4eadf95149b1d983a48f69b86b0cff927a7d6df13/propcache-0.5.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1d52a05dc417279f7e5c7618c5dfbbc29923aaf9bc0a5c1802ddcebf54c61a0", size = 50027, upload-time = "2026-09-16T00:14:53.67Z" }, + { url = "https://files.pythonhosted.org/packages/25/7d/c1ab1ef09e9d4d835be5d58c0a32a1e1de8397abaa4e502a9d4141328cad/propcache-0.5.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44149f46500a0a41b95b4d99c2e586a77319539730607b9892974a092788b111", size = 51425, upload-time = "2026-09-16T00:14:54.826Z" }, + { url = "https://files.pythonhosted.org/packages/73/36/0093091ebb270fcd1bc1f6e095f93b2e0ed7f1011c28837dc2dbe5f96b99/propcache-0.5.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbab5f5ff6897c81f355d079010cdae85b02e5a0b518b5251523b8ad8ae9ac3c", size = 233595, upload-time = "2026-09-16T00:14:56.09Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/0de9d4c8e05ce0be71b436919a216bd7fc5cc6e2691c0602295efb22b9ed/propcache-0.5.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3e98c55bde2bcf7db3c70d1aed7ae9aa8aebbf19a250c66645cde44cdb8b867", size = 240318, upload-time = "2026-09-16T00:14:57.674Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/2b35e91455209b85ee98f7859583e0814fab57d3af0f2381aaee34c37304/propcache-0.5.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db3ae52ccc150dbc84704e9d642743897f3e1c54742ff34cacb661e52e3818a9", size = 246649, upload-time = "2026-09-16T00:14:59.352Z" }, + { url = "https://files.pythonhosted.org/packages/ed/74/08e6c1faf26ee2732023a3828787ba535557122774f4a386b1f715cbd8e0/propcache-0.5.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f85915e00dcb1cd9f2f890ead064ed40a27df06f0db65be427b29482ae357572", size = 234316, upload-time = "2026-09-16T00:15:00.696Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/08385733c9321c9bb78039d3ff31045e4fca962d9665023c4eb70f998819/propcache-0.5.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c2ba30a89035b57b73e00475de948521602f543d79ce01db10b04b36c4c76fc8", size = 204666, upload-time = "2026-09-16T00:15:02.019Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f4/e87bc7629af9a14a752b218764a78742d73c2c563ac58315da6841f0cbe4/propcache-0.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ae58f361bd5dae942717c65d3413b478c70aea9c462599e7b9adad3731db3894", size = 225900, upload-time = "2026-09-16T00:15:03.394Z" }, + { url = "https://files.pythonhosted.org/packages/d9/6d/11014938d3fe9bea2ea2dcf930f26ed565bfb2f5be3c756362ea48c92636/propcache-0.5.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:96f7c5c15656040ddcbc51e56dc59b58aa25999d743c126abd425b9766ab43e9", size = 219988, upload-time = "2026-09-16T00:15:04.811Z" }, + { url = "https://files.pythonhosted.org/packages/dc/72/fbf17c589f92c0b3bbf6709a425661f8ef2ed0d46b38985a7d7b5a0f6b91/propcache-0.5.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7cc528e760a8af06f2b13e9b9f362cd90c7c718ea61228a96dbd31ba16ed7f47", size = 233611, upload-time = "2026-09-16T00:15:06.498Z" }, + { url = "https://files.pythonhosted.org/packages/55/7e/dbd637572a279692e5518d117274a9331bf5faac59f191d30e82521a3ec7/propcache-0.5.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:425f8cc86ab5018b4b8d4a23bc8e74d964bd3d757c3702e301aa79be76c53f6c", size = 204333, upload-time = "2026-09-16T00:15:07.961Z" }, + { url = "https://files.pythonhosted.org/packages/ba/5a/f99c92068f1e0f5c886899ce0e4a619db376ca98c5279d93f95bd86906af/propcache-0.5.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a5793c7698a53f56f4a1889a4737c7eeb1b7ad0842fa6b1abca22913ff79c8c1", size = 235177, upload-time = "2026-09-16T00:15:09.334Z" }, + { url = "https://files.pythonhosted.org/packages/ee/28/95456fabd2daf6be89049a13fbf03341756014d2959c83d12957d4c49694/propcache-0.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c02c0e570c5c7e077b0181a9f3cdb7d4c3617d1cda6b5c95bd5d34022923d82c", size = 228982, upload-time = "2026-09-16T00:15:10.729Z" }, + { url = "https://files.pythonhosted.org/packages/b1/bb/df90f62c9cf7c93ea235f6f9405143bba802914607317266dd81fc8d737e/propcache-0.5.4-cp313-cp313-win32.whl", hash = "sha256:3e413d7a4a9b4866b7a761d6060d434b64d23cd35122eda3b026a0bbe8196b25", size = 42611, upload-time = "2026-09-16T00:15:12.111Z" }, + { url = "https://files.pythonhosted.org/packages/01/bc/e0a7b84af04ec02d73a48aa71f091e1e4a2107e3074b7ce12195b66901f4/propcache-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:0c889f6fa84957bc7e8b4eab71fd16a0455068d5045e3aa40c733071d2b2fd77", size = 45342, upload-time = "2026-09-16T00:15:13.519Z" }, + { url = "https://files.pythonhosted.org/packages/9a/70/50b031cafe72a5c1878b903ee87303f71313345566bf3d6ec202e5ddc9ec/propcache-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:69fc35c0779522da366c563e5faf203ffc1f8ff0021d5b1337fa4efa5be73177", size = 42408, upload-time = "2026-09-16T00:15:14.788Z" }, + { url = "https://files.pythonhosted.org/packages/33/c9/07e227b930c8ae513b8ef1aae3793499be097bffcdf7aee4fb8b33db4cd1/propcache-0.5.4-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e6720ba44ad7e72174314d0e1fb0172494cff5c73a3a8a2159c3d2402ff15565", size = 85933, upload-time = "2026-09-16T00:15:16.073Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e1/6710bb44510c4e4a8e0f004bbaf3cecfd048141309c77bae56d4e5a6ebc1/propcache-0.5.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cfe0a92ae30151869e67a4b5f5e105e4e03ad30b3f38e5211b5bf77d0881993", size = 50179, upload-time = "2026-09-16T00:15:17.377Z" }, + { url = "https://files.pythonhosted.org/packages/e2/22/b533b493d7025456f44518b33e53e000021a20fe7c27b88cf3d341df7186/propcache-0.5.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d759d05634f1b038fb625a66662a8c85e5a8fec912da381b5149ddac107482b", size = 51942, upload-time = "2026-09-16T00:15:18.589Z" }, + { url = "https://files.pythonhosted.org/packages/f1/74/70ac8430e28f21e442c7bcb964eb46c4363f6881ade4aa0e978bfd8d503a/propcache-0.5.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:251c63dd46a0659bb875cb254dc4c1e79ee91a847c737cd62373295afc2235dc", size = 232647, upload-time = "2026-09-16T00:15:19.905Z" }, + { url = "https://files.pythonhosted.org/packages/72/95/f222f13b6fe623310be0eb61a673bf26df439ce27e563ca8e422d0818777/propcache-0.5.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a8d5ff04eb1f85698a78d20c62a14676e7b960dcafde09a388d60ad377d355d", size = 241541, upload-time = "2026-09-16T00:15:21.3Z" }, + { url = "https://files.pythonhosted.org/packages/a2/3e/763e370340db16115c5e63ad46e21ef0770a7f06928b3d3b62d8f8edfca4/propcache-0.5.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b9100a93b372418d8688f3f2a3e5b45c64d70ca4d6176e121aca1e3bfc1e32f", size = 245332, upload-time = "2026-09-16T00:15:22.802Z" }, + { url = "https://files.pythonhosted.org/packages/96/d3/e97cd6f5de2176bd90ed4076c7a9b5e09d0f0b9687d00a576507988bb62c/propcache-0.5.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc07876cfb079b6f6f36d21ce75784ad6c2c6b563eeac0ed26c2fa2669b85df9", size = 232757, upload-time = "2026-09-16T00:15:24.374Z" }, + { url = "https://files.pythonhosted.org/packages/f9/4c/6766e5f60bcda26d244333aa71d0a702c1c9b21b251d543c7af5953d1eee/propcache-0.5.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0951315a6b3142ee2167404d707743f0157c110091342b1aa0accac5cf0e4acf", size = 204389, upload-time = "2026-09-16T00:15:25.667Z" }, + { url = "https://files.pythonhosted.org/packages/b8/5e/ec4bb09a70b26ea99d76a8292c3383b960b296de2b347ac9986678f1761c/propcache-0.5.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bee7d3aed13d56f54e681df38c3a23031bc9e3863f687d9d598825c9146acd7d", size = 228217, upload-time = "2026-09-16T00:15:27.11Z" }, + { url = "https://files.pythonhosted.org/packages/e1/7d/b53922ba7d9e5bf797324e63aa05906ec240871899f779628df068743e2d/propcache-0.5.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4e985382be6d15da8d0c2710a6fa7b9070fc9ecdeefb7f580e88373984ec8be3", size = 216947, upload-time = "2026-09-16T00:15:28.532Z" }, + { url = "https://files.pythonhosted.org/packages/ff/39/b62eee45e5ea4de094a258cbb3b01c1e856ca51ddfd95b43135c5effd1eb/propcache-0.5.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9e9ab13760aa8b6d0881ae7cb04fd891d8d490cd2554ea8e79bb278399169bcc", size = 233457, upload-time = "2026-09-16T00:15:29.977Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a9/feec61ed296d993db9dd097e0f6723e3f576a647722367547495e4c5b05c/propcache-0.5.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1b2f3bec4261a94019575481c726c29850f72e27907773c75b1de421e20e9f9d", size = 204131, upload-time = "2026-09-16T00:15:31.74Z" }, + { url = "https://files.pythonhosted.org/packages/92/4d/411ef380cddad28dc001f1c6d75ec72c76cd3817030f68ec1ccfba0ec6c1/propcache-0.5.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:720cf832eb2d0b0dfee129cb3335a26f6ce3cc45ee1187e8f0731758caa16792", size = 234820, upload-time = "2026-09-16T00:15:33.087Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/c988229753629ef1cfd5198337a83e624780ea2b3787efe9e747c05aad2d/propcache-0.5.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fb0a5be8d9aa213150e8d8148a42aca4984b285bcad1e69587dc4298edd929b", size = 228350, upload-time = "2026-09-16T00:15:34.533Z" }, + { url = "https://files.pythonhosted.org/packages/12/49/5ef1c5cf98591da3c5b952b39e6a298084cc1ce353bc70f85e82397a5036/propcache-0.5.4-cp314-cp314-win32.whl", hash = "sha256:30cc1cebaf9aef49db06357a50398323ae04d70460c0491837d026ab7d6452ea", size = 43578, upload-time = "2026-09-16T00:15:35.957Z" }, + { url = "https://files.pythonhosted.org/packages/1e/9e/a0ac821a2229186af5e2e3c3635a78abb23cfddca57f38513ab5d70420f3/propcache-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:0a095db8e15a6020db149ecbed6461939fe74f6acaa3ae8b702a1fe8c38cd983", size = 46304, upload-time = "2026-09-16T00:15:37.655Z" }, + { url = "https://files.pythonhosted.org/packages/a1/19/c8d0d36a9d16cba5dcee67d389c9333b988c8986a653a61c00a451817a46/propcache-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:45488d1a5f9ab5bd90aaa1ca20f50fe1922b8ffad71a2009d2adf41355897aac", size = 43440, upload-time = "2026-09-16T00:15:39.091Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e9/42f1da77cacfc184e6ec929557ef653b7961bbf6f1da460b9221273948b3/propcache-0.5.4-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:53eaa697c4d0422ff4cb714d00231b43352064d97b944033b30c1d57cc506ec0", size = 90672, upload-time = "2026-09-16T00:15:40.306Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2f/4b79940908c6ab8c795097c102999d7bc1f7e0b8604dfd1c232f9d99d67a/propcache-0.5.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:886b59c4d28ca97dd23b025fdfc50a0356be934efbbbca89ad26230067f86fe5", size = 52586, upload-time = "2026-09-16T00:15:41.575Z" }, + { url = "https://files.pythonhosted.org/packages/eb/07/02196ae6320c110235bb343f90dbd34be41f8b8964a3ee30db84ec12579e/propcache-0.5.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fa15757fea1dfcd5b7745cad9f4638929605531bd4018ab2adff7955f1a403d", size = 54335, upload-time = "2026-09-16T00:15:43.027Z" }, + { url = "https://files.pythonhosted.org/packages/6f/44/f48b9a131985659924df5fa5093f68fe72c7ee375329802989ba3126efc6/propcache-0.5.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f0093ac3e9daada202c2082439d414a625c57184727a46e112a3fb2a81cb788", size = 297567, upload-time = "2026-09-16T00:15:44.373Z" }, + { url = "https://files.pythonhosted.org/packages/04/a1/418d956d2735139f77fc35262179f1f52c23aa666de5a8ab3819c1ae7854/propcache-0.5.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3cd3a7edb6b95b9b33998135ebfa18d709da82290fb8f27c858970b5a12c8b56", size = 297477, upload-time = "2026-09-16T00:15:46.048Z" }, + { url = "https://files.pythonhosted.org/packages/69/fd/ff811fdb6d3d3e67fd9bbfb75881675d34a42d0ef29a45d33e3e233dde07/propcache-0.5.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c174bfd1c48a1b51a3078e95586dde718374bac79719ab3541ec9e74aec40574", size = 302669, upload-time = "2026-09-16T00:15:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/fc/57/527910c455b5ec62f6871bef45d4f79fea16cb8c966ba0d4a07f0339ddc4/propcache-0.5.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a219f0ac59817a9114dd2aa57c13180f993e819ba658c7ddab4b66ed1ee0d370", size = 287908, upload-time = "2026-09-16T00:15:48.99Z" }, + { url = "https://files.pythonhosted.org/packages/1d/86/f69ab82707534a0cb2057bdca04f9200a71214c7551800f9d34d6ac39e4f/propcache-0.5.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:17a7400cec0256f0a71ae71f9da398f9894c956ff6668a1c9d317b3367316320", size = 249804, upload-time = "2026-09-16T00:15:50.486Z" }, + { url = "https://files.pythonhosted.org/packages/27/19/60677af50d93be4256213de7cd487f056944c048b9c0b6f2e45b3a30f666/propcache-0.5.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:978f28401afbc76cdc3df9e1717b4229a06b626a1dcc75db4e1f2beb3884c3e9", size = 282344, upload-time = "2026-09-16T00:15:52.029Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f7/a0057808a91fb3b6a5f3602b528f0cdcb3d53e0ff8315d73fabdfdf8fec4/propcache-0.5.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4a1f4f5ffa55dce6307631f3cb2948e117e665966ea512e0d502b16c24f567e7", size = 270167, upload-time = "2026-09-16T00:15:53.466Z" }, + { url = "https://files.pythonhosted.org/packages/83/c8/f4a865490df0dc0c8531d4e59ac411cb6dc24bb255d2396a6f1c60a368f4/propcache-0.5.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:213bb68d9ced5cf2bf717b1071bf2b09b4b04c426256f9fe6d054c60318424c4", size = 286551, upload-time = "2026-09-16T00:15:54.995Z" }, + { url = "https://files.pythonhosted.org/packages/b0/67/b4faebde9da4e8173d0e5a30e8cd31335914af7ef350b988f27fec588cfd/propcache-0.5.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:286867fb156488c251a3721766e380ac4495e4fd6b51aaa1403d89ce7f4359d9", size = 249595, upload-time = "2026-09-16T00:15:56.505Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/52e1dd5636e9f5a27f6b5a4b4e2f33c322fd72afe956c397d82523ec4a80/propcache-0.5.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:445ee3bfb46e85838387fb3c536a73cc0b994dc192b004e40e170adc54aa2a7e", size = 286700, upload-time = "2026-09-16T00:15:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0e/30b2b324b93ff31a0bab539c102aae59e84e444031b2742150a7646aa1bb/propcache-0.5.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:48cb48c5346a97de792254af77715aa2529c2a1ebc5f586aa0aae44a02f1fe57", size = 280500, upload-time = "2026-09-16T00:15:59.487Z" }, + { url = "https://files.pythonhosted.org/packages/64/36/721bb59f682ff060d0c8df64274fca8cd0521b1a54506c2eedaef795b7f5/propcache-0.5.4-cp314-cp314t-win32.whl", hash = "sha256:03b229037d25b801e7af53fd52b9fc49d9439b036fca1e087e02780631adfa97", size = 46121, upload-time = "2026-09-16T00:16:01.349Z" }, + { url = "https://files.pythonhosted.org/packages/c1/86/0b1b80fa1ac3a0aac44e2922a6964fbe9cd52af5eab8fa933bf9e90b030c/propcache-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:8a1fc236528c457cd739c88abe823da851b7ab645d72792f88658114cc340c12", size = 49154, upload-time = "2026-09-16T00:16:02.901Z" }, + { url = "https://files.pythonhosted.org/packages/69/4f/9fe6f05a47cb550c823155052116f710064b6be5c6e8ec4e9faae7e18115/propcache-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:135036c5cfc93864affb0f9af9a27e5d7a71cb7bd745e7b6dbfc2d56cc30e827", size = 46005, upload-time = "2026-09-16T00:16:04.266Z" }, + { url = "https://files.pythonhosted.org/packages/58/25/895a11d1e4c5c2acc6d816e2bece34e02d9dc92f2182ae276cd819e9e804/propcache-0.5.4-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:45bf2e730ab8905d0527fe05a86500f406e64305c34cc81ebe64b4617cab9760", size = 85634, upload-time = "2026-09-16T00:16:05.599Z" }, + { url = "https://files.pythonhosted.org/packages/58/41/c0acd69271de7a1cf439e77d5d60c18575fd09bad56e798b95fa23458ea4/propcache-0.5.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:31eb43ba2edc704ab2ec27815315dd8a19def0fb16215be4cfe8d32fe78ffd51", size = 50084, upload-time = "2026-09-16T00:16:07.384Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1a/ad561f99f90884089e6403b76c220610809429ba868a81a2e7ce115d32e0/propcache-0.5.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:174507f82d3594622acb1dd2dafecf2d899d6d506335494e7107767bf05f3aae", size = 51692, upload-time = "2026-09-16T00:16:08.956Z" }, + { url = "https://files.pythonhosted.org/packages/e9/07/057bdd3a9609ffad59b06239cceee784b047f6c720247bfaa36d2103e138/propcache-0.5.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e337653721d20ead710da33bf44487fbe8a0db8782714b60306481e9f95b51", size = 232947, upload-time = "2026-09-16T00:16:10.466Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dd/d36ad35986718530498a65e45e3713f9f0e6a580f192ef02d2ef7cae9b52/propcache-0.5.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d21d0d2c82bbfeb1677a9711f38df968f9837576102bb4add1bd449d28d88f1", size = 241250, upload-time = "2026-09-16T00:16:12.056Z" }, + { url = "https://files.pythonhosted.org/packages/fb/81/f1459415cdb6c10d46942779de39bb59a77b38e5a76bb1def9227962eb45/propcache-0.5.4-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccf4f7a79e26bb7efb06ecd50c177833b71df05cbc748701372325e6bcc17f6f", size = 245150, upload-time = "2026-09-16T00:16:13.596Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4e/58b9b1460afc97a4c0b17ee89af701c4011d4d7f46470eba3aaff76a8069/propcache-0.5.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23278f808cd81d5ada7184a76606b925fb3389c60e1077b2cd7da7b1fcf0553c", size = 232166, upload-time = "2026-09-16T00:16:15.126Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c6/5a79e0eda3e7b6987d03d8c622ff6d52a42165a12e8418eb37694b9cc4b4/propcache-0.5.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e738ab81179510ce79b2eac9a6ecf47feffd9e76d1c72e403005dddb6e36c06c", size = 206085, upload-time = "2026-09-16T00:16:16.713Z" }, + { url = "https://files.pythonhosted.org/packages/4e/72/940aed42c73f9da345ca2de0f6e835c726498159abca5f1ef14fb0a2af8a/propcache-0.5.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:a419ee85e654927baabda3929c03c0cc1112bf472ff0dfd6142f4e3a81ca4162", size = 228460, upload-time = "2026-09-16T00:16:18.352Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/3f54e1535c8f323d91ba566044d7c2b39ff6f6a2f1d0bd9071779d07b9b3/propcache-0.5.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:b61805357d966680acf68b3b6d49772631ed9df44ebece10ff1460e117a7da8a", size = 218350, upload-time = "2026-09-16T00:16:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f4/025890cc389ac3ec485ecec607d4a7ca47e15bfa2a465746ab98af602536/propcache-0.5.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:58134228927cee6c047d626c08e60a81be604a20578a12ce752cc5c9a84d4826", size = 233156, upload-time = "2026-09-16T00:16:21.624Z" }, + { url = "https://files.pythonhosted.org/packages/04/29/b39cae08c87c140d3d274f0a2c058cb5588e836175c3309e260b230ab07d/propcache-0.5.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:350b272b2279f4135a64fc0c304a5d08e28a137c9573442c606152446638a831", size = 206206, upload-time = "2026-09-16T00:16:23.204Z" }, + { url = "https://files.pythonhosted.org/packages/18/61/e16462ef18a87247dc9ebbd5c606f46d5ce67e708bd9cc734dd0d9222564/propcache-0.5.4-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:45bebbe252550fec975ba3b62bc6f931643cfd3b5464ef47619cf3fef154e01c", size = 234469, upload-time = "2026-09-16T00:16:24.841Z" }, + { url = "https://files.pythonhosted.org/packages/9f/84/b6a1490922427204fc47df920ed002eec709621de6b79b11592bf45c623a/propcache-0.5.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:ada748108a43d29b7c328ba7db3755327cd94f028bcc1a7ee3f0addcfacd9c38", size = 227311, upload-time = "2026-09-16T00:16:26.549Z" }, + { url = "https://files.pythonhosted.org/packages/ff/5c/5a59527582e9bcb694b2f08b9894134b65a0f5f79dbff174f054f5f74ed0/propcache-0.5.4-cp315-cp315-win32.whl", hash = "sha256:ee19113bce2f3acd46432050688b70f61acd6857d75abb9ec96341b7e9ced123", size = 43512, upload-time = "2026-09-16T00:16:28.313Z" }, + { url = "https://files.pythonhosted.org/packages/26/07/93cf699ed363681e754d7c3fad587fb09ef6b65618ee193332ad16a68d7b/propcache-0.5.4-cp315-cp315-win_amd64.whl", hash = "sha256:ceb3e879afac028f93d272c957814695dc5569e4904262dbee92f6c41bd5e4a3", size = 46264, upload-time = "2026-09-16T00:16:29.751Z" }, + { url = "https://files.pythonhosted.org/packages/65/10/fef04fbdcd44a4a163cb5ff5674599c6d6fdefd64a5a459438f9ad2ba042/propcache-0.5.4-cp315-cp315-win_arm64.whl", hash = "sha256:c83acbce9f2b5e3f5f5eda9e53d2001fed22fcdfef81274a9e02d8fd53b70a30", size = 43395, upload-time = "2026-09-16T00:16:31.5Z" }, + { url = "https://files.pythonhosted.org/packages/70/f6/7e2f4dab0b92ab46111bd48cee9ee1e5f519514c44e3779ede5358d7ada0/propcache-0.5.4-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:a5e8ef588c109725dc713ba69aadcac00a1ef90c2ce9c0a8c7075128f569f47f", size = 89825, upload-time = "2026-09-16T00:16:43.115Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8b/dfeff925cb6ced97ede701d5c6a99998da963c6f2e06abbf879c9dac5b54/propcache-0.5.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:4d86476a935c88963d9b8e1a9a0d38188790e9622169bfbafa173046846709d3", size = 52159, upload-time = "2026-09-16T00:16:44.754Z" }, + { url = "https://files.pythonhosted.org/packages/24/6c/924c810be5b7cf218ef47e707cf06d34adb4e3f3a31e3c24c55c6d945a88/propcache-0.5.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:f5470694918830da62fac9e69133b53d23b736d7070e587b27a4a2be37e08e68", size = 53956, upload-time = "2026-09-16T00:16:46.762Z" }, + { url = "https://files.pythonhosted.org/packages/3f/b6/9ed0a5c939b58b6bed740a05b5d0f919f0b318d03284b4b6d81a0fe8a29a/propcache-0.5.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10ef33a68a61ce317e095fd2e202a592ea92392b90944a78c993f0d9a73ab06c", size = 295235, upload-time = "2026-09-16T00:16:48.577Z" }, + { url = "https://files.pythonhosted.org/packages/5a/eb/5ce886e902a2e781dddf110993d5329458a9b1a8626b876c65e5e25bf413/propcache-0.5.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5cacf3c9efd09df409dc33654dd077e1c245ba8fb747b0f0236ef41b7c49b589", size = 294463, upload-time = "2026-09-16T00:16:50.539Z" }, + { url = "https://files.pythonhosted.org/packages/f2/88/c98f49183ecd3e5b204a556f0ca47baa02c2206a500fe8c7ec1726297b0a/propcache-0.5.4-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:770e8209d018175fc0063936fa9583b6d27e88c5ad31543f3383d66080efdd62", size = 300081, upload-time = "2026-09-16T00:16:52.423Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/c5090f9e6f67cbc30a2b744c7bb0f8006dcba5ec1b0d82f866ae1cc7c5c4/propcache-0.5.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03969626faf0783a592dfa17e28eac06018bd0b44dafae6943d53b92421a7f72", size = 285360, upload-time = "2026-09-16T00:16:54.141Z" }, + { url = "https://files.pythonhosted.org/packages/ac/9c/34a55396910583ed07926669ab309dde2213a2dec05a7e946bb90ad66908/propcache-0.5.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef3b928d9c984322b5c44e6964d8dbc653da87d2d8ee1647fa6da43072e650a9", size = 248014, upload-time = "2026-09-16T00:16:56.062Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b5/c0a142b656093ca397039dd3fe166cbb87c945712b534546514a24cd2611/propcache-0.5.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7177c43eddf10a0893c4fec52ebb408fdcd7f7d63962caace9180d8f81b14ece", size = 280662, upload-time = "2026-09-16T00:16:58.044Z" }, + { url = "https://files.pythonhosted.org/packages/54/28/fab2809c2e337fe26becea9648e84d5cef46075c91b826acb13e4f9dd04e/propcache-0.5.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:420162a77f94eb1cf5ef7893f500016dabd548e73de956785a1dd899cc73006a", size = 266149, upload-time = "2026-09-16T00:16:59.702Z" }, + { url = "https://files.pythonhosted.org/packages/3a/11/7ddf336288b2678a5f054f8da2e2bd1a719f5d4b7de714d9c6bd588a2313/propcache-0.5.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:3eb2e820e8e2101407da93f17c57cbb7d225461955fc60105daaba14cd421ee2", size = 283097, upload-time = "2026-09-16T00:17:01.459Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ae/351b1a5225f5473c411d9a612a229ae147cf0cf65c72ad838b87219ea8e8/propcache-0.5.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:13e52b6e0bde97dee98ab66552dbff2931649c96f1ac432eac299fe689ec373b", size = 248160, upload-time = "2026-09-16T00:17:03.298Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d0/7f79f061e30d135bb615c9782c94a74652033d00b49254edbbf35a9165a8/propcache-0.5.4-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:12682126712ddc19b70ff819debbd279e58adf1f0c8f8f8138c18ade2044b284", size = 283036, upload-time = "2026-09-16T00:17:05.238Z" }, + { url = "https://files.pythonhosted.org/packages/53/3c/016f1cad8bf4c428d748cf399b2bac603026fbfd6966e6a5579b5c5b6956/propcache-0.5.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:3af0c8642b2da4815d86e631232ac8286e17644fad907c19508aa8e7cb4ba8ad", size = 279350, upload-time = "2026-09-16T00:17:06.881Z" }, + { url = "https://files.pythonhosted.org/packages/ea/60/d8f72cb24b412487ed4c397f539117d3b74c3c33dd32020e91fe00a958a8/propcache-0.5.4-cp315-cp315t-win32.whl", hash = "sha256:1df8d8561b21465c5dd56110a01caf897e026d065b4b84e98a488209094272ec", size = 45874, upload-time = "2026-09-16T00:17:08.567Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ef/8bae0a316d406644450522f2f3d44a4e19632f5f3bb60d1d0e6c53842616/propcache-0.5.4-cp315-cp315t-win_amd64.whl", hash = "sha256:02c0a34f16889cf800f10f0247a564d8ce6eeab6ffcd7c87198f769067eb8432", size = 48574, upload-time = "2026-09-16T00:17:10.077Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/bcc053f66a97355683884b448198e79580fae8e8fa4d96b9bb01614e9913/propcache-0.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:dc4242ca653c9b30ab51c5f8193323e7bc0928f897ee9103201e59a43abcb72e", size = 45625, upload-time = "2026-09-16T00:17:11.377Z" }, + { url = "https://files.pythonhosted.org/packages/f5/cd/785c64ed382f3f04201870267b02783f63b4678c2acfddc177a3ebcc2727/propcache-0.5.4-py3-none-any.whl", hash = "sha256:62c60aec739ed00124573cce1178138fd690c7676352d67a37328c1cf51d7468", size = 16338, upload-time = "2026-09-16T00:17:13.106Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/6b/8f79692844269427abb3e4dd9e68edfcbe65ae25527d99183214de716c59/pydantic_core-2.46.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:657b40d6240c0a7b6a64b30f22d1e3aa631c7e846c621b0c0f6d1d75e2e15ea6", size = 2076533, upload-time = "2026-08-28T09:57:35.421Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d0/c787604c71c2bdcda1a5656942fc822cd0f9cd879b9484bb84fc42172703/pydantic_core-2.46.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ecb42011e12ee19cafbc312887cbf3546959fe02fbad44f272d4be5baa997615", size = 1924650, upload-time = "2026-08-28T09:57:37.944Z" }, + { url = "https://files.pythonhosted.org/packages/4a/77/ca2f8e997d9bfdb32205297aff38f210f398822d895b1af1b59fd9df9c13/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dedce55295becb61921e386b99d4f2706045306e7fa52249a33004c837379fb", size = 1951261, upload-time = "2026-08-28T09:57:39.339Z" }, + { url = "https://files.pythonhosted.org/packages/a0/53/bd12e1a9255df4edee00353778e2614b5346265d51e1567ab72153e803a2/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f47b8a949e60f027f0aa0a6f6c7b7e9c55cbf4380d10b344e282fa4e7ab1e1b", size = 2021808, upload-time = "2026-08-28T09:57:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/d7/41/f7f312751ebc6d6767da91964a9c7954c18e226a1720ab234e3dfb9d6c17/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:200aa3dc9f8d54f0754f43247c0bad0999fdcfbfd2488384dd44f37279271fe6", size = 2196184, upload-time = "2026-08-28T09:57:42.275Z" }, + { url = "https://files.pythonhosted.org/packages/3d/93/ce93aa030ab6bac4683ba8861e7baad89dd24b02e66b8801a0e4f6a00311/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d30e1a4f138b8951063e9a394752a9179b51da288ffa507b1e659222f4c1793", size = 2238212, upload-time = "2026-08-28T09:57:44.122Z" }, + { url = "https://files.pythonhosted.org/packages/34/a1/c8e6b66f499f510752c07a092dfe27621f9c255635e59d38704b5681c35a/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:850a08d167dde16db8702c274f320c7be9d7da6f6dff2b58b18f9e815bd94f5b", size = 2064073, upload-time = "2026-08-28T09:57:45.613Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/605e2b127ee30dbf4b1da9da4843587cf2b2d16486c241cc7a5be2d2c1bd/pydantic_core-2.46.5-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:c3471e5c4a949c26ec00a77f01df59096aa9495877de76fd60a980f8ee6be461", size = 2093102, upload-time = "2026-08-28T09:57:46.953Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f7/1ab28093c09032ddce7c92c7a55d503b6ecd70f42c32492946c1cb5477b1/pydantic_core-2.46.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a3e26b6a8274211bddee2d0e4d0d42778f17a34510f49d2ec44b58abfc41736", size = 2133452, upload-time = "2026-08-28T09:57:48.362Z" }, + { url = "https://files.pythonhosted.org/packages/30/c8/47c79b756f12f85e8b0fbdb2b495f6b6eb32e6c98a4beae7a570a0b7c63c/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fc5d783bd4a2387e97b8a2d5ec781cfb92b3d893bf82370548e99db5915935d3", size = 2146477, upload-time = "2026-08-28T09:57:49.74Z" }, + { url = "https://files.pythonhosted.org/packages/13/5c/79fc00cb8f651d6061991de8d7cedf1c78c73cbd4862c42ef418f03b8bfa/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:356c8368cbc321050b169595683a2e1d63413b1e0e2868b330af9fc14c616d3f", size = 2300832, upload-time = "2026-08-28T09:57:51.639Z" }, + { url = "https://files.pythonhosted.org/packages/b4/72/dd1a29853cf6d22a1ebd9e3baf0239cbc57d2d16caff36a89e38eb9b1db3/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb7d8d0e5886a89a55d2eef490e272fa965a9d57c6b29a5b5088a7997ec2cad1", size = 2320505, upload-time = "2026-08-28T09:57:53.236Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d1/ba4a8e06a9ddad0b4caf69cfaeecc0fbfcec20473bd808f5127fd16491c4/pydantic_core-2.46.5-cp310-cp310-win32.whl", hash = "sha256:4d44cf99ddebf875f9b68cc267aa684c99b7b44fe63ee1cac4ec163807290069", size = 1956853, upload-time = "2026-08-28T09:57:54.592Z" }, + { url = "https://files.pythonhosted.org/packages/f2/94/205ed9d7ddaf44acd489889708ea124a3f41bdb42c141c8684d528ad0e7a/pydantic_core-2.46.5-cp310-cp310-win_amd64.whl", hash = "sha256:1e5aad1220a1192c42341c8fd4a8686657e73ab2a920c970bdc4de334fe3193d", size = 2042551, upload-time = "2026-08-28T09:57:56.017Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b6/81d2d19ea0be2c03664381b59f65fa72fc7969decedae00bc2c4ad835708/pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f", size = 2074737, upload-time = "2026-08-28T09:57:57.711Z" }, + { url = "https://files.pythonhosted.org/packages/0c/18/b70da8300e292df4099684ea11b1958043580d2f50d2dc8bf7e542bdd84a/pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f", size = 1921751, upload-time = "2026-08-28T09:57:59.265Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1a/0d590341b6ffa4b4aca83508e6b8db4761aaeacfc15a25ca3815876d4797/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061", size = 1948231, upload-time = "2026-08-28T09:58:00.678Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/02eb35761c51f2f7b1b042d6ab4cda6600f0c8c88a2243b3f734376201e5/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be", size = 2020708, upload-time = "2026-08-28T09:58:02.267Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ea/f86073830e35d508cc8ddf9c3d9e6e6840fcb88d34bf726b0b4710186f27/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a", size = 2194914, upload-time = "2026-08-28T09:58:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d7/fc36240d7791ce90939e51608568c33bfdae26202016f9770c229a487d86/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b", size = 2235622, upload-time = "2026-08-28T09:58:05.516Z" }, + { url = "https://files.pythonhosted.org/packages/cf/bc/3fa2d76b83162820a17da7f645b28d1cba99fc8e1e5fc6517067ec450fa1/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c", size = 2062091, upload-time = "2026-08-28T09:58:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9a/095d557bb492c90cd8a70a6dd048bf793d433d03d86c81c11e912e4cd049/pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee", size = 2089904, upload-time = "2026-08-28T09:58:08.814Z" }, + { url = "https://files.pythonhosted.org/packages/24/98/7b76b1ad10a19a617a52aaa1d80e159115af939b095e86f8e756fd52e0df/pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e", size = 2132244, upload-time = "2026-08-28T09:58:10.435Z" }, + { url = "https://files.pythonhosted.org/packages/20/32/7d6ca365fadba186a0c8f85de1a701663bce81efd309d9479be58687622f/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2", size = 2143901, upload-time = "2026-08-28T09:58:12.033Z" }, + { url = "https://files.pythonhosted.org/packages/f8/09/eb9a6aa57f22fd1541a9c0aa2a1f3aeef3ec65347d33e10a6da2f43e0ee9/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689", size = 2299425, upload-time = "2026-08-28T09:58:13.614Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f9/548a5bb9d4ba8cd26e26daf48052236f6b38bb61e7b7241fbc3c995719eb/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec", size = 2318566, upload-time = "2026-08-28T09:58:15.199Z" }, + { url = "https://files.pythonhosted.org/packages/4a/20/06454d18834c02c406c9133f1a3b485305fd9ee984f9636c2f730bef6a9d/pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129", size = 1954258, upload-time = "2026-08-28T09:58:16.813Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c2/718b9deb4b72453b5d8c7447a3b14cb77bef36917ef5f514e0948a4096a0/pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c", size = 2041030, upload-time = "2026-08-28T09:58:18.288Z" }, + { url = "https://files.pythonhosted.org/packages/67/ea/c1d1a5b72d6e1ff7f377a4d9199f6591f095beb5b409a8a5d89f7238d939/pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8", size = 2009234, upload-time = "2026-08-28T09:58:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/76358795aa7a8c6d4f36e2cb828ad1c90ee118e1393a9281664f5aade9d4/pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d", size = 2076516, upload-time = "2026-08-28T09:58:21.576Z" }, + { url = "https://files.pythonhosted.org/packages/db/50/26b091836076ce4cb2fac264186936acc069e0595772cfd02a563bc4761a/pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e", size = 1922874, upload-time = "2026-08-28T09:58:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772, upload-time = "2026-08-28T09:58:25.435Z" }, + { url = "https://files.pythonhosted.org/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832, upload-time = "2026-08-28T09:58:27.102Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645, upload-time = "2026-08-28T09:58:28.948Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935, upload-time = "2026-08-28T09:58:30.625Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284, upload-time = "2026-08-28T09:58:32.289Z" }, + { url = "https://files.pythonhosted.org/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889, upload-time = "2026-08-28T09:58:33.986Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006, upload-time = "2026-08-28T09:58:35.647Z" }, + { url = "https://files.pythonhosted.org/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408, upload-time = "2026-08-28T09:58:37.38Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609, upload-time = "2026-08-28T09:58:39.22Z" }, + { url = "https://files.pythonhosted.org/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618, upload-time = "2026-08-28T09:58:40.89Z" }, + { url = "https://files.pythonhosted.org/packages/81/5e/468fc630568c61dcef3cd47ad32ffbeed9af643f49208d1ea86ab4f890c4/pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b", size = 1939475, upload-time = "2026-08-28T09:58:42.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c9/4c19f41b84cf6b622a72fbeed7665b25d47a187d68d47d0d430c07f23268/pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8", size = 2043140, upload-time = "2026-08-28T09:58:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/af/dd/0c1a050299147c746e5256db16d645ab5efd4f78c59937d581a0524e74a2/pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084", size = 1997729, upload-time = "2026-08-28T09:58:46.13Z" }, + { url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" }, + { url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" }, + { url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" }, + { url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" }, + { url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" }, + { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" }, + { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" }, + { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" }, + { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" }, + { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" }, + { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" }, + { url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" }, + { url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" }, + { url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" }, + { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" }, + { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" }, + { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" }, + { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" }, + { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" }, + { url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" }, + { url = "https://files.pythonhosted.org/packages/af/1e/ecca01fce348f7e8afa9572441ff6f7d1cc70d21e4859f33944d10877e1e/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2", size = 2075342, upload-time = "2026-08-28T10:00:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4c/af80c7a8032dfc897040ad5cb772bebde529a381186499e6e29987f23f8c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c", size = 1907219, upload-time = "2026-08-28T10:00:53.438Z" }, + { url = "https://files.pythonhosted.org/packages/be/3e/54d89e2b092e778716bf6153634ef479e955f48c261090be23aa1e0fb0b5/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47", size = 1953393, upload-time = "2026-08-28T10:00:55.58Z" }, + { url = "https://files.pythonhosted.org/packages/ea/89/828ee90cda28ce17bdefaa3a6eaf74fe430e113295a10e6126beca559d6c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a", size = 2099024, upload-time = "2026-08-28T10:00:57.794Z" }, + { url = "https://files.pythonhosted.org/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540, upload-time = "2026-08-28T10:01:00.318Z" }, + { url = "https://files.pythonhosted.org/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040, upload-time = "2026-08-28T10:01:02.529Z" }, + { url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" }, + { url = "https://files.pythonhosted.org/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034, upload-time = "2026-08-28T10:01:07.345Z" }, + { url = "https://files.pythonhosted.org/packages/20/21/22102e9950b3049526d20e811b95396508377d87651edd2b80d2b3d28659/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f", size = 2071333, upload-time = "2026-08-28T10:01:09.636Z" }, + { url = "https://files.pythonhosted.org/packages/d8/18/87aefa427d191e6d3ab1447f1efc1cdcac86af1069239b133e8a0fd7f7c9/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0", size = 1912713, upload-time = "2026-08-28T10:01:12.285Z" }, + { url = "https://files.pythonhosted.org/packages/1f/93/fd89e9ad49b1805ca94d24ce1088b7d305f05c35ffafcedb9819d03588a0/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4", size = 2090926, upload-time = "2026-08-28T10:01:15.19Z" }, + { url = "https://files.pythonhosted.org/packages/6f/45/8e59dab6acf8d35f02f0a958980074f31038968bdb2c983fcae9d1efee03/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25", size = 2131303, upload-time = "2026-08-28T10:01:17.937Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a5/e1d4dc5180dd887a9522efc1f8716b8692b7606b1d3273d7862eaf66be44/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6", size = 2145128, upload-time = "2026-08-28T10:01:20.694Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/ad493864a7fb21c0c4df98f965e2db430cb25a9d7369b5778d5016c09fd9/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e", size = 2294560, upload-time = "2026-08-28T10:01:23.495Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/b41c84c913f29973a268e6c2b5bbf13c95adb9956c126d10da11ba3b2bef/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda", size = 2317531, upload-time = "2026-08-28T10:01:26.334Z" }, + { url = "https://files.pythonhosted.org/packages/db/1d/068464f23075f66a8f1b806935e9cd9363ee446636ea70d2c22ee8659dbf/pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266", size = 2140686, upload-time = "2026-08-28T10:01:28.947Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, +] + +[[package]] +name = "ruff" +version = "0.16.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/49/23802c45f093eb14bde54b141d2b2f058edfa63a06db7beed047308cc08f/ruff-0.16.10.tar.gz", hash = "sha256:eff4728c4eaae93f0955cd264d24b2ab348e74bf59986ccf282ba6dc16b3b017", size = 4958724, upload-time = "2026-10-01T18:03:21.697Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/21/ebce22e1d90cdb2cd691026b9c6e9bec6499f0b396089481755b5d49efff/ruff-0.16.10-py3-none-linux_armv6l.whl", hash = "sha256:488b0fe3f3574210e5cf80d9f59b9e3ab17a127a8155de3f307b392589cfb511", size = 10095557, upload-time = "2026-10-01T18:02:36.072Z" }, + { url = "https://files.pythonhosted.org/packages/cb/98/a54de85876a8b2612bfa0d84c7b9abfb39c6a3354aee7800b09c1649b9e3/ruff-0.16.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e748ff95c934c4e978783b8e687bc174e7bd84e8ad24e3243e1ecfcda5e0282d", size = 10340577, upload-time = "2026-10-01T18:02:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/9f/16/1a5a4a2657effe4806110f2b907313802f1367fcdbb29e8122766e407fab/ruff-0.16.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3031a4a2e8e7b8a46f70be45f198c35a11ece509a94b80334d8d397a33c67550", size = 9774282, upload-time = "2026-10-01T18:02:41.79Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/470085af734da396e68cd80fb0a3e7c109a459716ae59588c0f6fab8a17d/ruff-0.16.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:494401c86df4c4c25f69b9419605d944467ee98c42fb6ad405ef4fa40b8fb67d", size = 9920895, upload-time = "2026-10-01T18:02:44.458Z" }, + { url = "https://files.pythonhosted.org/packages/57/de/f10cffe4f88a37ea6615ec460f01bf76f0bc1477737a35d9ee61a630a78b/ruff-0.16.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d203abc0ff2b773ee33d00ab8df0bb67046fbc7c07b119332f08b9b345cf8221", size = 9902707, upload-time = "2026-10-01T18:02:47.041Z" }, + { url = "https://files.pythonhosted.org/packages/ff/44/3fdcedf83ae60ef837dd239170e480dce606a9142cfa2db911afdf855fd9/ruff-0.16.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd83d1235a5258d318477bc5b576303974cbdef5df0c01a1bff14efcc23bd12a", size = 10619287, upload-time = "2026-10-01T18:02:49.485Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/02e76a5574002153618eafbb70e159468addc72a5d2c488e65cbb4639d2d/ruff-0.16.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc2610fb269fa56dd8a68669ae470fa6272902668c0fc2ebc3aa112b2633d5b8", size = 11339942, upload-time = "2026-10-01T18:02:52.008Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c4/cde27d47ad8d4126c587e608c67c46e7feac11763f606d261a1bc8a489e6/ruff-0.16.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e70175e29cc94c26ea296c80e470180b744b7419026898e58f520c6ab32578e", size = 10934316, upload-time = "2026-10-01T18:02:54.811Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/17234145f302a645a123e8c3bb2411ecf4669fbc350de3b0d803230a1729/ruff-0.16.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f33f43a864a8483eebd160e713336c8bab02c934feaff0a33cf5ccb41546d09a", size = 10387968, upload-time = "2026-10-01T18:02:57.497Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/2493af60240ee7644b38a4b821f5f9c3b5a4fa3178770fe1d0c685217395/ruff-0.16.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1dfc6f0088149fb6a362c1c446bcbb3fd2157b3852fe2fa68409276eab9ad9b3", size = 10537906, upload-time = "2026-10-01T18:03:00.006Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9a/f56b28f3b143bb9e518c34fb89ab191b626bca8b70dbf8af0d2e2d473572/ruff-0.16.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6553498afc35f580f036030795810b9e6bcea31604b0fd9e8d352795473042e3", size = 10012938, upload-time = "2026-10-01T18:03:03.006Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/fca37362848ea4d4d80712df13632e645e7c7cfb1bdedf140699ac7a090b/ruff-0.16.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a3b8471dea115d37f123882be852bed13403746d3a76c11de4a19ec5f9ff5a03", size = 9897945, upload-time = "2026-10-01T18:03:05.818Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c7/e0bc57664d6e0c61fd22f620af260fe9662d7e1ddb320ea7f160e76ef165/ruff-0.16.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:92e59a70bcbd9d3a5483656da906ec28edfdacfce00afd99edb8b4e9d15644be", size = 10333134, upload-time = "2026-10-01T18:03:08.25Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/5c9f3b68737c0e4a33a1db5dfab44f7b786d96ca91a7233112ddab1e6dc2/ruff-0.16.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7ae7375f803b5520dc9f546bed7e3a0acb70b91812e9bb4b19927de22f25b77d", size = 10741702, upload-time = "2026-10-01T18:03:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/78/fa/0f9c2020dc316c53d983be011720b8157cc052b97e3f4dfa7db6d0f880a6/ruff-0.16.10-py3-none-win32.whl", hash = "sha256:2a12e01cb9156c10c466f63b46eaae5ecea28dfbd21b5836353ae498e7d1349a", size = 10139176, upload-time = "2026-10-01T18:03:13.267Z" }, + { url = "https://files.pythonhosted.org/packages/99/29/cfb0df9448d4d4ad48c2de029ada9ebd71baa6da983a6c77ee6c6cd0fe82/ruff-0.16.10-py3-none-win_amd64.whl", hash = "sha256:97f2015c92aa97105b0eab19eb5d224884399281cfc5da86a92db4ab5e7fb2ca", size = 10584734, upload-time = "2026-10-01T18:03:16.006Z" }, + { url = "https://files.pythonhosted.org/packages/fc/05/c16957eb287c3fc062e032619a25868d93d408a844b725bcf514f7a378ff/ruff-0.16.10-py3-none-win_arm64.whl", hash = "sha256:25a65fe998c4e6861ec079ada5826a2fc605e6cbccbe9dcd7fac1f54e791621b", size = 10366440, upload-time = "2026-10-01T18:03:19.04Z" }, +] + +[[package]] +name = "supermemory" +version = "5.0.0rc1" +source = { editable = "." } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] + +[package.optional-dependencies] +aiohttp = [ + { name = "httpx-aiohttp" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.21.2" }, + { name = "httpx-aiohttp", marker = "extra == 'aiohttp'", specifier = ">=0.1.8" }, + { name = "pydantic", specifier = ">=1.9.2" }, + { name = "pydantic-core", specifier = ">=2.18.2" }, + { name = "typing-extensions", specifier = ">=4.0.0" }, +] +provides-extras = ["aiohttp"] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=1.13" }, + { name = "pytest", specifier = ">=8" }, + { name = "pytest-asyncio", specifier = ">=0.23" }, + { name = "ruff", specifier = "==0.16.10" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, +] + +[[package]] +name = "yarl" +version = "1.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/16/e8be8e2fb175bbf41a0680381a319f1199fae256588241a2ac8677eafb49/yarl-1.25.1.tar.gz", hash = "sha256:03dd38de09bc213e9a8b29761eec33ee1d5318dac0e49d8af36e4d27830e23a7", size = 246245, upload-time = "2026-09-15T19:35:02.264Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/92/55fa9ee84cb8ec9930a910b5753936a926f918d8cc8965bdac571479c095/yarl-1.25.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:142c06c4d6a35ee3ec5da08499805e879cb3ca7c1fbfbecb0140fe72403818d6", size = 144695, upload-time = "2026-09-15T19:29:53.114Z" }, + { url = "https://files.pythonhosted.org/packages/16/b4/9edc8e605b16b2eb5bd0c2ccc73e2b15aab583862460767fb43f91f11839/yarl-1.25.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:24ce942011a61953e7d313438038f4d32ff21387b775f58a957f7a07dd55ef95", size = 104332, upload-time = "2026-09-15T19:29:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/ae/de/4204e6646e278b1cd4a9cf73ebe8b7cfcf16693f1249b324eb273d67602a/yarl-1.25.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e23c82b63cd7652fc24d33ed6cc17099d607aa3b4fc4ddc75e95062f3d82df4", size = 104449, upload-time = "2026-09-15T19:29:57.239Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fc/21d74789198ad68a23a209ab0d73fe5a52d39ebbd9945041e72827acdd77/yarl-1.25.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ee202350cf57abf0e9502a41601841019c25d3db7ff52d980aaf31446254059", size = 116828, upload-time = "2026-09-15T19:29:59.031Z" }, + { url = "https://files.pythonhosted.org/packages/8f/31/90891ddb61848c067ebd1fb505ab5902a6e7d4707c2f3823f67ff37883d3/yarl-1.25.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5df89f769cc8ff94c3d7e7603386fba309d25ce5240132d26c15baa8d0e96c4c", size = 107150, upload-time = "2026-09-15T19:30:00.868Z" }, + { url = "https://files.pythonhosted.org/packages/a1/15/f3d18743d3688b5f01aa83034b6c00665692ab20e1d0c89a29000d4979ea/yarl-1.25.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:83e9f4a25085bd4b7214701a0794ff1f50fc633ffb8bdfebf07abdd81c2db126", size = 124704, upload-time = "2026-09-15T19:30:03.128Z" }, + { url = "https://files.pythonhosted.org/packages/99/37/718789d8004775d6a2db86b2afc72b1d156e0590b88a1b8634ddd4bdb8ee/yarl-1.25.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e636b64d24fd9c38053c5e389a1174c66361fa49dcfd220f4dd35b4abde7cb89", size = 129246, upload-time = "2026-09-15T19:30:05.014Z" }, + { url = "https://files.pythonhosted.org/packages/28/b9/7818ac6dec7fbcd16be2d19495807c01e2c38834e57d41c63356a75e786b/yarl-1.25.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e5637ca8d0bd7fb72648a6c7934af4baaccb697657f7438c9d264fc2abb8b0b1", size = 118071, upload-time = "2026-09-15T19:30:06.977Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e3/8d098cafb30a64df7283b5a5dd0d32d22a71b18a1b9ec071d0776ffe5c2a/yarl-1.25.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:683e362b8ba453080f7489c66f4ea794e751c35b72e7eab3575ef784c2fbc7fb", size = 116180, upload-time = "2026-09-15T19:30:08.836Z" }, + { url = "https://files.pythonhosted.org/packages/20/9c/fbe9432478d87e00eef80242e15179639df162cb3d5d8d978fc56dcb6c51/yarl-1.25.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:df23df54b5114a17c2d0ef192433e2e5a9f0c5178c32375e90b7cfc965f349d0", size = 116578, upload-time = "2026-09-15T19:30:10.782Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8e/b3dfd03732236b86b0bcf06a72a37de412ff6f82e719a947c3c651d4dece/yarl-1.25.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f53dcd26694f148f738edc052b5a69234833e739f10f4c3287bdfd8ec0f7b326", size = 108886, upload-time = "2026-09-15T19:30:12.951Z" }, + { url = "https://files.pythonhosted.org/packages/83/e1/94dacb650d4963d5f844bc3df7ba70288cb7b68ed5a4d070da71acb19b14/yarl-1.25.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b8075fe90bc08e40b8b8a1874fab42ee4c7b56af05c5886e9cc841397f916908", size = 124098, upload-time = "2026-09-15T19:30:14.963Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3f/c93f76a218258c7bfc60dd27fd56e9102bdbc625517f4a6ee673295fe4e4/yarl-1.25.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a8c2b841478068440d8b733005d13a5ef535b9928cbc05f17182d410f32ba449", size = 115768, upload-time = "2026-09-15T19:30:16.736Z" }, + { url = "https://files.pythonhosted.org/packages/a8/79/4d93f13c3b05cda3c962805dec28cbc255c50239b3457808abc5633a00c2/yarl-1.25.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ca32926d7d77bcc8838425c4c95e040a3ace1cb7dfdae599013458dcda2607ca", size = 122461, upload-time = "2026-09-15T19:30:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8e/e2ba83b3a9bfc1d3b882ad35fa8abc04a7af4d9356f2e329a23d0c8d889d/yarl-1.25.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:192a866877a49993949ef1975864ad8728bea28ee810f6abe1a0729c2b500426", size = 118295, upload-time = "2026-09-15T19:30:21.07Z" }, + { url = "https://files.pythonhosted.org/packages/42/67/cb5ea1baa0c0ac60bda44ce59f601133154af2a2e67722d3517f8793855d/yarl-1.25.1-cp310-cp310-win_amd64.whl", hash = "sha256:3f4d48a6112712973e676bd792121fee470e432d749177162d9949d5c9460a1b", size = 102929, upload-time = "2026-09-15T19:30:23.122Z" }, + { url = "https://files.pythonhosted.org/packages/2f/85/9a1e98de10fc0e738d517efd2a9984c3e7db6cf35fa72b2b373988a9e9b7/yarl-1.25.1-cp310-cp310-win_arm64.whl", hash = "sha256:48796ea00a303961507dc6c8437c4b325a6fc3f95f7c36c71b91ea9a8150963c", size = 98854, upload-time = "2026-09-15T19:30:25.102Z" }, + { url = "https://files.pythonhosted.org/packages/83/b3/2cea721d495ca57f8f414aa4867ee263f486b274e07463158cf52f02ba7f/yarl-1.25.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9d693bf4bf534e9ba3ae2780cfd577f5135629f7b5ac653490859d0b77864865", size = 143794, upload-time = "2026-09-15T19:30:26.946Z" }, + { url = "https://files.pythonhosted.org/packages/55/e6/cd145cff8e5cf60b8b3c41fbecfa2a45028a8dec3fbc52bec03595ff3d3b/yarl-1.25.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ab2054c5531af2a9ba7b69b8ec91e4f884420e83a8c5e579b013084cb57e5e5d", size = 103993, upload-time = "2026-09-15T19:30:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7c/fbb40fe2d53747c40aa36a9e2bd2178a202f942bda0b670f3306d4aefbce/yarl-1.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:564fdc7085d2245ab84f88882fdb1d6ac0723124bff6ded35bfb1c00f812630d", size = 104010, upload-time = "2026-09-15T19:30:31.069Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0b/5a516f70641092283f57cf3670bdb75e7327bcc0dcb5038697e4dfbfd569/yarl-1.25.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acae6b45d1ace09b6ba3876da43b88366ef368f73b988c7f57e14231753d4420", size = 116444, upload-time = "2026-09-15T19:30:32.894Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/d1a627f827b0a404ae0f5647cab0534959081cde13b0756a783469fb3b5e/yarl-1.25.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1fb2a01ba8cd9c5d2c5dc1ec35e0fc951d04b4f037541d4ac090c993ce58b3d7", size = 107565, upload-time = "2026-09-15T19:30:35.188Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cf/c8e0aaec886840a6c4480fe44eaec7cd4319f79a563b79321473be79c56f/yarl-1.25.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e92b6bcc741b86d67606c40d3cb9c7cc8e6c737f81e31f4a94efc204456c92e3", size = 125006, upload-time = "2026-09-15T19:30:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/50/26/0cce366d54a93cdc8342965dc7663385e4db161625cc1e8b18e786753d24/yarl-1.25.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:72c34ac7ad4314c19362d5ce27626dcc8429bd30bbf8c179f4234078851f9492", size = 128717, upload-time = "2026-09-15T19:30:38.904Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/a71501bbc1a674ff72c4d6c2b75f4d9a5af819f5244c3a7558080a8802c5/yarl-1.25.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5add7b4ca7afeea91d52e4d4e4db3b1fe9885b71f07054560d8c4296b7441a2", size = 117728, upload-time = "2026-09-15T19:30:40.809Z" }, + { url = "https://files.pythonhosted.org/packages/e7/6f/c3267ca01defeed9ed9c4ff9b17bd54915432c405945233265b707475d1c/yarl-1.25.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:def538065f9e4d4cf1ae164bd59aba00dfa84f03923e0de4c3788f252d6bcd17", size = 116224, upload-time = "2026-09-15T19:30:42.818Z" }, + { url = "https://files.pythonhosted.org/packages/8f/69/fad57ee52d648431718ee0f1f68966a99c1352c3924688ffbdcc9d3fe51a/yarl-1.25.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a191bfdb30a79b98e5d175d75285f9fcb78bf0e46ba5efda042e1c72071a0de", size = 116299, upload-time = "2026-09-15T19:30:44.966Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d4/6a8c1e29f33338687ca278cba0a8fbf6525a322c2c02a9a500ccbe041152/yarl-1.25.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:71f42c5b9a948c113bbdebfa544598321431d064ff959d32e99b1feb61d68345", size = 108625, upload-time = "2026-09-15T19:30:46.874Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d2/3a35ae791c9cb6522c106923ff25c3230d999091e5e65511bca23bbd9914/yarl-1.25.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:72849d892954be4d09e569b8b831ac39ce58417fedc767d4308a0fe542018a40", size = 124515, upload-time = "2026-09-15T19:30:49.082Z" }, + { url = "https://files.pythonhosted.org/packages/be/fd/2b022109a6b4af0f7dc371cf7500af380b0d4f034010243e1b0ce218dc93/yarl-1.25.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:efb01a106f971cb3752856bca2318bbdf7f01bd8823779c461586cbe5ffd5258", size = 115711, upload-time = "2026-09-15T19:30:51.208Z" }, + { url = "https://files.pythonhosted.org/packages/18/59/f7586271136c3ddb0126bbfe661844699369b76b555fe38c4efe86870b2e/yarl-1.25.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a1daf47cd95a7c3a63456336bc5aaa8c86dd3a47d07ed3d0e76132ae4666a5a1", size = 122751, upload-time = "2026-09-15T19:30:53.535Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0b/07f7a2d881f7e16c385b47fc1753382600847cad305dcc7fa0c25828acf8/yarl-1.25.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9489e6abf47ba37f332075a91444c7cfedb03e6ce99fbb2f116bfe1ce810da3b", size = 117983, upload-time = "2026-09-15T19:30:55.277Z" }, + { url = "https://files.pythonhosted.org/packages/aa/9d/8cdceec66a9b940700cb45931741403f045b162afd79bcb93c41cadd0972/yarl-1.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7306dee25b8a0e737363f347362b875094b4dc4e367311470656ae420fdbf8e", size = 102894, upload-time = "2026-09-15T19:30:57.606Z" }, + { url = "https://files.pythonhosted.org/packages/17/f1/7ec357db1d3ad2863542d71e8fe64a126bbec17b134cd7d30951196809a5/yarl-1.25.1-cp311-cp311-win_arm64.whl", hash = "sha256:abb1384477f5901d436b5d2e5465954de46ea6098f59163d243660b5c4461d35", size = 98609, upload-time = "2026-09-15T19:30:59.705Z" }, + { url = "https://files.pythonhosted.org/packages/75/b3/cd32ac66ae622b854c2df0ac52106dda220d361b65a64fde7d5b3684aa3f/yarl-1.25.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:94d7aa6debf92a1dd14cb5280b083a764169a13cfb23a452111160274ed989f4", size = 144798, upload-time = "2026-09-15T19:31:01.821Z" }, + { url = "https://files.pythonhosted.org/packages/61/fb/a2c52a8007c2051ba74662afb112ecf3d00346af4c25e33df9d80fd14fb8/yarl-1.25.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:83d4a37e4b95da4d8bda930d6d35b75b4cdadbacbb4980cae290ea3100b5d51d", size = 104583, upload-time = "2026-09-15T19:31:04.05Z" }, + { url = "https://files.pythonhosted.org/packages/be/dd/ee38aec8e09fdf957e50d4085453fbe202f56c6c3b4cf07b81cdb4f09ee9/yarl-1.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e029648f9c951db30e98a7d7ec90835db88ec4b32820efe2a9bdc2287e032eb6", size = 104325, upload-time = "2026-09-15T19:31:06.338Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b3/058dbfb1857b484c9cf9cc135659f50b85ce66e03c99e44dc2f7b6161f55/yarl-1.25.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d781294bb815ecb5ea57ff6bbf8038e0a31a95fdf3e1788f66e0dc100d64b58", size = 115358, upload-time = "2026-09-15T19:31:08.593Z" }, + { url = "https://files.pythonhosted.org/packages/db/39/29693446cf0cf6b15a0e2f75a5d40f93c56819b05b0622196f45e95b5cc0/yarl-1.25.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e12c538e00e7c1b286a07061046b90e8124e6a9793efae2c70db6a4aad07faad", size = 107658, upload-time = "2026-09-15T19:31:10.802Z" }, + { url = "https://files.pythonhosted.org/packages/86/b3/3c4dd7e1af43b931fba95e0a722737f2ea94a6d199c802585282831d7abd/yarl-1.25.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e4de3ac4adbad3d0bc7c6f4360a7dbff5de2f15e3b723be3198074e17fd9c40", size = 122660, upload-time = "2026-09-15T19:31:12.84Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b5/1b60dbc3cfc9c5712b15148c206748f2bc93953ffdbe25ea75b63dfc89c9/yarl-1.25.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:419f392a1da624877975709e3864dfe833af6cc7671b39318086d456e288380c", size = 126506, upload-time = "2026-09-15T19:31:15.088Z" }, + { url = "https://files.pythonhosted.org/packages/bc/7b/ca212cbe170ac8b96e45317ecbcf9c3c3ecf0cdec98d5b088a9c4088929b/yarl-1.25.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6f117789d22dce188e5754e8bc65b7e6ebf8cb73963b9fa761f672a5883769d", size = 117050, upload-time = "2026-09-15T19:31:17.241Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c3/72b4938cdbe619ad71ac156182faef4908846b84dc3ca4dbb4c4e6f84014/yarl-1.25.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80e47012e730da131c9f059c80936783f9659aae22dc31c03c0595590d11ed54", size = 114174, upload-time = "2026-09-15T19:31:19.294Z" }, + { url = "https://files.pythonhosted.org/packages/e8/43/268717870f9ba0cc9701a95181587f6dc8c5f387aab4aeecc83158f38a79/yarl-1.25.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e80f557716fd765439577131e526b8942ffc2c07bdbc5e39fa62f660ba1e963f", size = 114944, upload-time = "2026-09-15T19:31:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/da/84/baa5bf504d51fe062c4bcaf62936da97fffb43285978d0b39984824231fd/yarl-1.25.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f61964f235a43738bfac50da46fc4254943a7eea3051aeb0b6fc7c992c29fadc", size = 108263, upload-time = "2026-09-15T19:31:23.388Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/779a2ed9e0152a601a27039bed9aead3f0b79797a67e2c44bfa444622dd8/yarl-1.25.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e546fe1d4a93ebc2910f0d768baff19faa09843ab3f2036a67ed6e69fae4419d", size = 122184, upload-time = "2026-09-15T19:31:25.343Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1f/118e9e5b8f07694d63fd3222e801d7782270003f1a222aa798df3f8d5933/yarl-1.25.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cce0727fd5ac04d372fa9bbfde9febc2bcf209aadfcf0468e45dec72719895d1", size = 114001, upload-time = "2026-09-15T19:31:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/a4cf1cf372313734b17996d4007f9f73596e7a178b9485802e5494ecf484/yarl-1.25.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af4ea5b37403ef4e30f3927eaed540db942bde01d8d3ff083527c0704d1c9c68", size = 120565, upload-time = "2026-09-15T19:31:29.47Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/ad94f93ca731bc9e44d321833ab96b82a4f9f5f63cf773f81a4aeea5ecc1/yarl-1.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:68782fdb4027b8d1eee25ec35e9a6db05e863b899eb0310b3a33b6c3fef55707", size = 117060, upload-time = "2026-09-15T19:31:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/bb/cc/51a7b4abf4ac593b8e7eb3794b28e5a35ae26eed8bc04787628d215af82f/yarl-1.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:7d575b54cb3863ef9bc290ea4b009999d55dc237326131e4853cf33e888fee03", size = 102593, upload-time = "2026-09-15T19:31:33.329Z" }, + { url = "https://files.pythonhosted.org/packages/9d/21/0941a6b93a58b59a1ec75e5333bf06929b671309c43c0cd201c172d9c39f/yarl-1.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:bc3ac7bf569f6b64dad04dd7808c7872dae8a97df657856eac05e9b7e3614a85", size = 97697, upload-time = "2026-09-15T19:31:35.855Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/2f3129bbcc9a5c8ba12cc2b29d8060a3bab9c8043c456cfd4b5ca3188890/yarl-1.25.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:25868beca8b6765f8f7d0e11fe6dd7c66dd4b0793b9500286d20cc92352126a5", size = 143623, upload-time = "2026-09-15T19:31:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/f1bc3390fdca352826676b531d0712736f156919090206700421d46b2c37/yarl-1.25.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:10b2fd95332f0d716d5eee3c9fb2ce8eada19082de7fee83d32e37992fd75c26", size = 104011, upload-time = "2026-09-15T19:31:40.25Z" }, + { url = "https://files.pythonhosted.org/packages/a8/aa/50acc5c3e5da04172ae3c281c75405af4d2ca911e16120ab0563f4dffb66/yarl-1.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f12afda4eea8c8994a76d4df1875c765194f5fbe8a9d197929ea303caee29ec", size = 103677, upload-time = "2026-09-15T19:31:42.46Z" }, + { url = "https://files.pythonhosted.org/packages/30/d2/7d1e0ab9f8390e1fbcede5a6dbf70d23c96ad09b8c5567f3a514d1ddb0e2/yarl-1.25.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14b79a30a93a3ce2e8832603fd0ab780ada281b0ba5110b519a634f2d7d7d1fc", size = 115392, upload-time = "2026-09-15T19:31:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/71/e1/5ba1e3a2a22139213655e760919038e8ed7e2d4a99826d0bbddb3beb96e5/yarl-1.25.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bd6340d20ae2c7ca719b87b426e808e90743b676d05d4c26c4fb5ca71f41184", size = 107493, upload-time = "2026-09-15T19:31:46.273Z" }, + { url = "https://files.pythonhosted.org/packages/f5/53/780653d5e0f73831f467cf13548912e5eec97f21dc49fc8daf21da027df4/yarl-1.25.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:126a2533570c554719ca40a1288fdee1700b6bc82e7131aa69fa85252d92e651", size = 122537, upload-time = "2026-09-15T19:31:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/03/92/d54fa70236c6036271c9c9c09fd978df5cbe3ef49ef6c46e9b833476d215/yarl-1.25.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a3faadac7d812ddac258feb57b9846b60c1b437c4f4b9ad42595c6f6fe4390df", size = 126170, upload-time = "2026-09-15T19:31:50.872Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b7/a82a49bf88340b837ef6972b508a1604ae377b9e6904b46b10cf5f1cf925/yarl-1.25.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be80550d9bfe83d9b62398a37081a90434e6df2d978ec345c3d2820de6beddab", size = 117012, upload-time = "2026-09-15T19:31:53.189Z" }, + { url = "https://files.pythonhosted.org/packages/ef/78/5d684b411e3f3602464ee9b538db48205038f8605872985f61efb809ced0/yarl-1.25.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e07595c7d6f4db270ceede356a1bd1c07a34f1c26f958d1ed0cd7b48e0d2bba3", size = 114950, upload-time = "2026-09-15T19:31:55.694Z" }, + { url = "https://files.pythonhosted.org/packages/2f/11/51d82b852c64f7fad0fc7a7ff3031517204887e874c722bbca839c0b23ac/yarl-1.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eb96ed1ae6c7d072d60840c0434aef07a2df611812810807fbc54263a6053e9a", size = 115428, upload-time = "2026-09-15T19:31:57.966Z" }, + { url = "https://files.pythonhosted.org/packages/e4/49/9d1978049bf646b9ea918313926453c6901b71c92f097467777d47d36a88/yarl-1.25.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3feb99222553a8cbedfa52c2f59dd84c3f50d5b582c728d522caf8d72769a54b", size = 108428, upload-time = "2026-09-15T19:32:00.048Z" }, + { url = "https://files.pythonhosted.org/packages/43/35/7b8f1ebb45d7ec3dda7d1909bf44f458de41ef91e2937f107733582a5166/yarl-1.25.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a2ed0ba415ccdf08f14bf544cb78346d0f76086707ffee24921a2c84dbf1305a", size = 121961, upload-time = "2026-09-15T19:32:02.436Z" }, + { url = "https://files.pythonhosted.org/packages/63/d6/d8b689ab7ca26edeb85f6ff28812aac7a25376eefc1780e303a7bfbaceff/yarl-1.25.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2b49375d22299b0a834c2bca72f39aaecc270d96fb24c30424899676f487b22a", size = 114961, upload-time = "2026-09-15T19:32:04.456Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d5/1a1798ea4dc6b7ee3260010a27907ebc697c95dae99817d817ed446d24aa/yarl-1.25.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ef74070ac553c59eb4f04258722066d6c6135b7baa03b2e9f2da65c096e96d98", size = 120036, upload-time = "2026-09-15T19:32:06.5Z" }, + { url = "https://files.pythonhosted.org/packages/91/8d/b1b35ed7903da6669b1d367cb2c09436acd4ff508029b4f39a0c0c2058fc/yarl-1.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0a66db89ea473abeac4b70523cafd94db3772380e565f9d28af7a179b7af71fa", size = 117276, upload-time = "2026-09-15T19:32:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8f/4db01cef62caff0d7a4593ed694fb8a41a27a11158cab80d290221f13e57/yarl-1.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:1f51020b2eb8a003c84925638ec63c21a750a4bddd3a22ec8eac6a742dadf1b9", size = 101945, upload-time = "2026-09-15T19:32:11.545Z" }, + { url = "https://files.pythonhosted.org/packages/c0/5e/3ce00497c5c0babb74d4130c10c3828ccd215b4819d12020c42429f991ac/yarl-1.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:b10dd0557ba422715b5206b3743192135a6022acca8baec51aa127d0a75db8fe", size = 97270, upload-time = "2026-09-15T19:32:14.127Z" }, + { url = "https://files.pythonhosted.org/packages/80/cf/54023edfab7aa773b860503db0c56e962ccab0922803ee97988c176ea090/yarl-1.25.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a9ca696eb02e5c02a8afd872ada510eba9b7fe6e68b9572c2e9a9b1941e31e2e", size = 143975, upload-time = "2026-09-15T19:32:16.416Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a8/e6c1be0e6761d0f2d10bbf33a3e1e02b99dc83874d92945d7b461a72481e/yarl-1.25.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a5877f2255aab518ebe528289037699201d5dc5f045f2396cb30aa02db22f57f", size = 104018, upload-time = "2026-09-15T19:32:18.364Z" }, + { url = "https://files.pythonhosted.org/packages/6e/bb/dda344765ffd3430afe1a1c66c866a57fae67786537d4f14607df6505ac1/yarl-1.25.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7a5c3115595995779ee21f2567035793911c3802a43c74f3fbb0314929ec67ac", size = 104156, upload-time = "2026-09-15T19:32:20.459Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5f/ed1538bcd06009fe990d6d283dd7667f639e62a81e35c6d8c6ef6c08fb3c/yarl-1.25.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77e5099b99b37f3cf79c246998ca9f7313a78054cd1809ec46bc1afad47e1c4c", size = 116025, upload-time = "2026-09-15T19:32:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/a2/af/2185daf56b99830d3356ecfada46faaa49945de6626e842b7728088d4980/yarl-1.25.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6efaf45df6a849cef613a03a94c845647456662f85438c886bb67a9c027c8c2c", size = 106985, upload-time = "2026-09-15T19:32:24.749Z" }, + { url = "https://files.pythonhosted.org/packages/c1/65/bc1ae564fb4b04a30b6a8f250e787772581c57e4c3d5cf07ac3359de3103/yarl-1.25.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5f90e44653c4e0f78501ed9bb7d3fce835a8d62b7c6ed0cb16557534087e743", size = 123030, upload-time = "2026-09-15T19:32:27.084Z" }, + { url = "https://files.pythonhosted.org/packages/6a/3e/e2afcde10d74e53b3fa889960991efb3019beda2b1682a01de720a302056/yarl-1.25.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:632da579b2d879f6bad20f2cfa35ded1efe2f4f77f8abb26a6234a5b236acd2f", size = 126765, upload-time = "2026-09-15T19:32:29.332Z" }, + { url = "https://files.pythonhosted.org/packages/a2/be/415b00c0fe5a0615b062a456b26623d7ec91c2bee20faea1a14045aa0469/yarl-1.25.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:30eec96e8a91bd588ce897c9543f6d5d8d34b28fbcba28a4dedf20ebeae9fe57", size = 117199, upload-time = "2026-09-15T19:32:31.49Z" }, + { url = "https://files.pythonhosted.org/packages/97/27/3d8c63ddd3e8bcfd033748ab93876678ce59bacd66e4cb1ed851c9c5b37e/yarl-1.25.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:12b6bc4906e11f5e1a1cdcb12296e7afbd366c783cc8073403cd2fb74334e453", size = 115187, upload-time = "2026-09-15T19:32:34.137Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/7a81d0be1a502a26a0d4326c6f2ecb736c824f570ea1c6529f2b0b227b50/yarl-1.25.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9d6ed3d17bccce4c05343e1ca8da13bc5c02c812a4e7282ddd05e8769322d3fc", size = 116085, upload-time = "2026-09-15T19:32:36.438Z" }, + { url = "https://files.pythonhosted.org/packages/f0/69/39fff459916aa0fab42215dc47b759586fd80f94aa56dfc4a7c15ba6e0dc/yarl-1.25.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f38a70074041d3b7e138e452799f5174198bae5bd5ab2000917badf403908c5f", size = 107996, upload-time = "2026-09-15T19:32:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/c0/39/80b9a55a3335590451d9ecf3eb593a8c635351f4c905ef056d7e8a8fd9e7/yarl-1.25.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca89e4e21854ed27ec753297dde84b16c9f8e53b14a4866fb44457d643c19f8", size = 122549, upload-time = "2026-09-15T19:32:41.151Z" }, + { url = "https://files.pythonhosted.org/packages/42/7d/a179c6757818bb59372a4adafd09f7f26a3b4a0f04c3ae404b544c0b0c82/yarl-1.25.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1ab7618921a93767387a4b83776f751588f5b5ae9bb5bc96620e2e2e00bca868", size = 115107, upload-time = "2026-09-15T19:32:43.072Z" }, + { url = "https://files.pythonhosted.org/packages/32/2b/a773ac867e4ab53a98ed98e5cefe3bae31e6f550252ca9d1de266f1a40c5/yarl-1.25.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0ae12ff2b805fa02c4dab838005caef735e39986322698c48588d3beacb65c62", size = 120666, upload-time = "2026-09-15T19:32:45.061Z" }, + { url = "https://files.pythonhosted.org/packages/bc/41/52be6505e85b0f76b4f85b01b5de7e06a0512201abc2c95e14e099549174/yarl-1.25.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90c30ed53546da833c700115c0064c22120d1b1560f474699fd31f22dd668233", size = 117505, upload-time = "2026-09-15T19:32:47.177Z" }, + { url = "https://files.pythonhosted.org/packages/f5/01/349c0386caedbbe488d519f252df54efac8a1459282d466c474bdd84a620/yarl-1.25.1-cp314-cp314-win_amd64.whl", hash = "sha256:acfa7e22aa6c6e7a5996a41d275bfa01efa7ea56ab890590280e9063e2cf5c1b", size = 103446, upload-time = "2026-09-15T19:32:49.615Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f0/8ec63180f77912f0dc4e5a42760cb8c08d20da1d5ace3578a01b84d1f3d8/yarl-1.25.1-cp314-cp314-win_arm64.whl", hash = "sha256:8e7d98cdbb6d71e726f7d525952867096053d1f290dd4e3c50d7d313a136f414", size = 99159, upload-time = "2026-09-15T19:32:51.686Z" }, + { url = "https://files.pythonhosted.org/packages/47/7d/92d2220d6886b70ab1ed8579533ac2af2dfac716d5d929001daff7986df9/yarl-1.25.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d21f0fa80a02d05299207eeaafef345d812ace96d5306e4ef265e1d419a615fa", size = 150071, upload-time = "2026-09-15T19:32:53.911Z" }, + { url = "https://files.pythonhosted.org/packages/64/fc/b245e448124bcda9340df38e3553fa222b50260fca027a84095e9bd8642d/yarl-1.25.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17c9877a89fb6e2bca6f9087eb24cd7fb434653946ef5075e470d23d49b52287", size = 106780, upload-time = "2026-09-15T19:32:56.443Z" }, + { url = "https://files.pythonhosted.org/packages/51/e2/9a6ce2e334ebf218a30335ae76fb1696459430d42f733b8cb0d7d65b84d3/yarl-1.25.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:29273edf1530e397bd07cb784db1fbe0d2590b77569f2e24679a9c0a2d763b94", size = 107361, upload-time = "2026-09-15T19:32:58.827Z" }, + { url = "https://files.pythonhosted.org/packages/ed/70/66e8c76b569b450d16e190f15071c916c3df70b0e33927e415ac497cf0c2/yarl-1.25.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7abffdf37af1cec6a2ad69b827aa84320db5894791bc8ed932dc93fb274b7e9", size = 114396, upload-time = "2026-09-15T19:33:02.24Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/0d82838a05c57fdc05bc8b66e8c92dcc0df15e27463a5f163142d521c682/yarl-1.25.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2239a02249d9326655419e0168a28ca9008938eaab31dc29fc875c217927a6c0", size = 104882, upload-time = "2026-09-15T19:33:04.494Z" }, + { url = "https://files.pythonhosted.org/packages/86/d4/ea08615c4edaa6049a13a2f1128944d068d1893abda7d708d4d7ea01599a/yarl-1.25.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:664ec6a520b74a1df2810666eb67695fcb77fa663e6ea0a25aaf2e529cb24dfa", size = 119485, upload-time = "2026-09-15T19:33:06.583Z" }, + { url = "https://files.pythonhosted.org/packages/1a/82/0898bdce9b1ae403b308b9c733d0d24af4a3464270c2c081f457b16c3e0d/yarl-1.25.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f1c91f5a5980a937ff8e238e98e6897e1ad74a4b1e2c0d68c73b5ffbb3f5c0b", size = 122490, upload-time = "2026-09-15T19:33:08.653Z" }, + { url = "https://files.pythonhosted.org/packages/d1/38/97d79b81c342b78246cfedb74809e68841f3198d21653e10d3232bd9c622/yarl-1.25.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c88edaec8c349ad4c5ad4c486a3defcc4b80ceb2f074436ffa0a87caf5e76a6", size = 115336, upload-time = "2026-09-15T19:33:11.056Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9d/2577896554cd310dc470adb6da0b7dd0b435cb63e2565204a7ac240e504c/yarl-1.25.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:35dcbea443fafb3eece757ad4e514560ddeb6c34cfae1582c620d7b293d7feee", size = 111825, upload-time = "2026-09-15T19:33:13.204Z" }, + { url = "https://files.pythonhosted.org/packages/29/6b/7ac49d8ba84a5c4bd73415a4c949d22c749cb3762579b3d50e48019a78aa/yarl-1.25.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:882569ff613758cac762a457a5d72d6e211b28d4bcfea89d1d71ea942b02eac0", size = 114655, upload-time = "2026-09-15T19:33:15.553Z" }, + { url = "https://files.pythonhosted.org/packages/e5/18/e5942a16723f5b72f9b1297fd5a85a54f6300cd15c0dcb5005b90cd89156/yarl-1.25.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d0f1489233a254bb3643d2f05de7d59019254d81daeca6b9162fe9edef57e0c7", size = 106395, upload-time = "2026-09-15T19:33:17.599Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2d/549fa46240781513ebc47ae7eb418df428a163a2a3d644cc9cbb3ecb7846/yarl-1.25.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f41753a76f4f63927d03a0d8ba8f5ce0f2083bec29a8cfaccc55371b1564b96b", size = 119277, upload-time = "2026-09-15T19:33:19.973Z" }, + { url = "https://files.pythonhosted.org/packages/76/16/4763f78dcdc0b3b9fb3842b04afe72b9320857c6a69300c62a0eab03d119/yarl-1.25.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fb0eb4955adf0579001581f2f71a126e8781ba61bcd120f127b0401163c6c2d", size = 112504, upload-time = "2026-09-15T19:33:22.464Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b4/974e3edfe0d188393ce1cb9de400111c63fe61f4eb3b772a500d84c970d1/yarl-1.25.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a1e32763e641a1566507d90a8d3b19bfc3cc04a9d4e5ae3e32189874ed4b58a3", size = 116243, upload-time = "2026-09-15T19:33:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/b0/aa/157b940428da80c104ca09666a740e51c94963df65d5b112e06b52e4d7a8/yarl-1.25.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65b5b2066651b7432d389e9799d979c703bcc6ef44266bb8153ef54e91e4aab3", size = 115822, upload-time = "2026-09-15T19:33:26.886Z" }, + { url = "https://files.pythonhosted.org/packages/7e/af/19fbdce41412e1b96825544cc52cd7029d3724655d0988237972f078bd29/yarl-1.25.1-cp314-cp314t-win_amd64.whl", hash = "sha256:734f6e5400352ac4254456003d462866c684703570929cff7a7bde015d0cb371", size = 107386, upload-time = "2026-09-15T19:33:29.009Z" }, + { url = "https://files.pythonhosted.org/packages/2a/99/f6431c8968e89be608d74b28ae2d024521b2953f27dd44e0dece5e04f67a/yarl-1.25.1-cp314-cp314t-win_arm64.whl", hash = "sha256:287e99ff5aa4dc1c7630bfc683ded6f106d756c99dec432a2d7f197a784f51c6", size = 102094, upload-time = "2026-09-15T19:33:31.151Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3b/4f51eab40c2eabea6c3d5b121dff4b8988dc35087732ffede12d2be8b8dd/yarl-1.25.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9b1bdaae98bc016825dd3c9d8ee1832f829b3341f9cc6ebd1a1b0a7fef7367cc", size = 143875, upload-time = "2026-09-15T19:33:33.52Z" }, + { url = "https://files.pythonhosted.org/packages/41/05/bbd58fc063f5f299a883f810760b265ca26c8167c91cb9a494d0fe2387e1/yarl-1.25.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e7011b8fb8c4054bf0c12e5edc6cd83778b0028e99ce59b18586ed036f92cfdc", size = 104028, upload-time = "2026-09-15T19:33:36.22Z" }, + { url = "https://files.pythonhosted.org/packages/12/ee/2fba0aecb52e7020e189f684148783aa0b9cfa3b3bfb0b400646eef70ad4/yarl-1.25.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f074e8d4aa0a5798920ddb6de3d08b228c614ff3724c3e8bd7577f4bafea867b", size = 104041, upload-time = "2026-09-15T19:33:38.86Z" }, + { url = "https://files.pythonhosted.org/packages/38/35/884beab53ed88c7247d1671972b5ef116f7351fe0c7e6de8c3558372cb16/yarl-1.25.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d42e7e3ca399555578b4d617e3a6ecf13371b3743a115995fa010c7bf341459", size = 116018, upload-time = "2026-09-15T19:33:44.265Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/1a91b685afb55cc18608443ace95280e96e263a97565811732b6788d3269/yarl-1.25.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aa4ed3dd308548f9e707d9caaf005d2d7f8c1e7868f858dfeb47fe76e16b391d", size = 107033, upload-time = "2026-09-15T19:33:46.45Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c1/58b379fcb1d68d907b7fcf75200c44321896509b2a6a74abbb4b19d864d2/yarl-1.25.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42a66563d8cc056ee32e6191e05097a7b2b3bc302e0bc3133daf8710eb18bd26", size = 123257, upload-time = "2026-09-15T19:33:48.631Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2d/1fe96cf5c2aeab10095e48f38585cf5a8451fb7253234822398e52aa5336/yarl-1.25.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:98d370568f393215d605304cdb77b3d5539bd192c75b623c7304c42c8d6d8273", size = 126745, upload-time = "2026-09-15T19:33:50.999Z" }, + { url = "https://files.pythonhosted.org/packages/ad/60/8674394ce43f4dadae573a1d6f451716438e9eab7d7fe8d643c673a32d85/yarl-1.25.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23bf5b403c879a54964e0feac7285688e04bb220074878d737d331522da0a5bf", size = 117255, upload-time = "2026-09-15T19:33:53.456Z" }, + { url = "https://files.pythonhosted.org/packages/2f/72/0faa30e02605d56127d42bb987dcc97da3863b7bf70b9bfbf5f739c05e30/yarl-1.25.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d45673badd08456d0340e9364eddafe1c53a9d2896424294de4d7dd71ad3ee57", size = 115166, upload-time = "2026-09-15T19:33:55.669Z" }, + { url = "https://files.pythonhosted.org/packages/8c/90/9a46eac564c437e128285c5c1d7bb385d268394e9209d84f6bf4a14471ef/yarl-1.25.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0136d640dfa9b0523853e411430a99f8a91eca85774c6420285a33b755bc6de3", size = 116082, upload-time = "2026-09-15T19:33:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/ad/38/4b1a686a3758878f93d2f1ea943f5a165f3555769cd16e761cfd0efdca17/yarl-1.25.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:59ba3a6e1aa8cfe5adf4bd270fd965db21955401b7ca6f1696010c55ed4daec2", size = 108048, upload-time = "2026-09-15T19:34:00.25Z" }, + { url = "https://files.pythonhosted.org/packages/da/14/f348eb967f31a58348612a2b93bd8a2ab664548e2b5e879cac7f592f7201/yarl-1.25.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:87796fedc3ba97ec14fab55acb48584276e6c1e4c1e89c422bda62c838e754a9", size = 122775, upload-time = "2026-09-15T19:34:02.455Z" }, + { url = "https://files.pythonhosted.org/packages/ab/e9/4f7b79700f88cb9e8bb66f8b54f9bce1844c013a2c39fdc47112e9334c95/yarl-1.25.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bd0912757081f89b107d6c00b2ff8a194401b0b87eadcf4481de2b865a8fd44f", size = 115096, upload-time = "2026-09-15T19:34:05.283Z" }, + { url = "https://files.pythonhosted.org/packages/cf/37/f9cb020331997d3eb887bd28d5410ecfd3d80bf163c23d7cec490d78dade/yarl-1.25.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:b51c159a9794633f5e0db7ecec7b2b6e3734eca1f5d17dc989ff3552a43ff78b", size = 120655, upload-time = "2026-09-15T19:34:07.382Z" }, + { url = "https://files.pythonhosted.org/packages/12/83/52fceb22891a41f168db7ec22fd1d81e06b6a0b8d9f70921bd3e785defd0/yarl-1.25.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:319e070a01db9920fb63761843f96a104c8e2b9427266731810dc1e22595b17c", size = 117488, upload-time = "2026-09-15T19:34:09.988Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5c/ce6c4ff1247fcbe4b33d462c23a097106d909b173fde7042bc52290466e2/yarl-1.25.1-cp315-cp315-win_amd64.whl", hash = "sha256:a2059a2d891bd156bc5184e7ab7a56e78a84dfcfdeac8c501b552533ad1c36ee", size = 103434, upload-time = "2026-09-15T19:34:12.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/766a906b0704fb26d52b19dc22bed48a8ba0544203b70dcf44da350e8194/yarl-1.25.1-cp315-cp315-win_arm64.whl", hash = "sha256:a78b50b4f7918a3de71105d5c0b93bbc57bb8339a4d03a9dfd449f9068e76f3d", size = 99155, upload-time = "2026-09-15T19:34:15.132Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d3/a1d09b32cb6ab14f66b44939f5b4255b8b9e747aef3974af1d5d80ccc2fd/yarl-1.25.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:b5402a340723fa7da00b5cff987ddab61276be6d11251ea71ae02bcac54890d8", size = 149284, upload-time = "2026-09-15T19:34:17.452Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/fe554d879692650bca70bfbc0df124e82e4d2bb7456f698c7756f1279a96/yarl-1.25.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:eda19ea5ee88742f47a2340816e6f2d40b53bed3ab5b69794769f36af9f35bb4", size = 106399, upload-time = "2026-09-15T19:34:21.474Z" }, + { url = "https://files.pythonhosted.org/packages/e1/4b/e7af56177ac8d40094c82d7728224c0b8472157d50d362e5fb3b014b2bc8/yarl-1.25.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:75baa6cf9b6d1c52f3e111a130e202fd8cf0a5b3a066c3f73d615e885092e4ec", size = 106968, upload-time = "2026-09-15T19:34:23.619Z" }, + { url = "https://files.pythonhosted.org/packages/da/4f/2df41fd738d46f23ef829ae8b4468d94bb6070038fc6dfab6165ed44fea8/yarl-1.25.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbcef5a9119ef653653132cccaf999b30a0af6f33bb0a4ba80bec30056868487", size = 114717, upload-time = "2026-09-15T19:34:25.879Z" }, + { url = "https://files.pythonhosted.org/packages/69/ea/002b66df53bbd1aed1c23358ff99c9bdc744fe3f4d2740e1b2fdd7192885/yarl-1.25.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7efc9f082dfed77c316edffa9deb52888e1bc6789171887cc1f68e06d65465c8", size = 105198, upload-time = "2026-09-15T19:34:28.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/7c/95c8bc0c8f97d71e59c94525ad60d76f5c57d3f2820f08137ca8b9f0542a/yarl-1.25.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe01645169a2112aa1d4ebc3e4c5f029c5c8f97adfc32e5d37c993b39a994d75", size = 120271, upload-time = "2026-09-15T19:34:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7a/6fe9da56ec77927baa669fd86c39c567ce6205bab53d581082c6744c8ae7/yarl-1.25.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1c557dfd5e3db046053a0bdc72261ade790ebe8e2c7a41b36b0ca1f14cb95f3", size = 123572, upload-time = "2026-09-15T19:34:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/8b/83/35f222d17fa70a14c7c74fdf112ccf5515e0c2a87082b1f9b99f7693bf57/yarl-1.25.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ce4d6ccafb33d39bd78444612d14938ead674c25702ded2ee9c54a47735d225", size = 115228, upload-time = "2026-09-15T19:34:35.344Z" }, + { url = "https://files.pythonhosted.org/packages/da/6f/fbaaf619423578a7d898d0f226ae47bc1906c293865c416d83c17b828b0e/yarl-1.25.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80a063f8297fc796296f00f100be520f209b23dc98f93ce8eba6ee7122598209", size = 111618, upload-time = "2026-09-15T19:34:37.656Z" }, + { url = "https://files.pythonhosted.org/packages/ba/78/7383278f1b3cf8e0496bd95b3281a7b09b89217b6b428db24c6b99b3deca/yarl-1.25.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7cb414a73e21a7ab58254926073f2930cb22f5b4314ea4260a687e2b3fd4dce3", size = 114839, upload-time = "2026-09-15T19:34:40.099Z" }, + { url = "https://files.pythonhosted.org/packages/62/49/5506e5b6d29aab91bd845cc9016d88c3d3f81b81bc242b8100bdd5737825/yarl-1.25.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:85a18376073f8a39aa07be34f9fc77e2869aa72c55c441efdd2cf79a0407504d", size = 106212, upload-time = "2026-09-15T19:34:42.768Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/19877c193b7c5929f4b07118c18bbe390f3fadd0f59dd98c0cd12b31fa5c/yarl-1.25.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:77716e245c90f058466a05e6a465bb8600f767a8f4b18b4d40f3aff958e5f73c", size = 119985, upload-time = "2026-09-15T19:34:44.976Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6c/0a46fbbf9ecbcbd0cc20d2193394254b9e19817f22c814aab60f99847400/yarl-1.25.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:1e80dcf1446e1b080b1932b0d103c464a04112f5bc31f0f983ad418172063cde", size = 112081, upload-time = "2026-09-15T19:34:47.45Z" }, + { url = "https://files.pythonhosted.org/packages/ef/30/93f5d471230c74ccd06255d0842739f86551f937f9e63a5e947853c6244a/yarl-1.25.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:bdc8d8b8c22e9e43ac68316b5e6cf083dec537f4ec213cb4aa967b583bc3fa64", size = 116995, upload-time = "2026-09-15T19:34:49.972Z" }, + { url = "https://files.pythonhosted.org/packages/2b/80/c386593035ee3f9c6c6af0847b5578f2830c674794a9d7701b744a3ebd42/yarl-1.25.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbf531053a0935f2e871bcd4753f90313688772ff8c017f5ea402e315a78c1f", size = 115570, upload-time = "2026-09-15T19:34:52.628Z" }, + { url = "https://files.pythonhosted.org/packages/38/02/eef443559563ef8f2e10469387b8b1e97cb5efee95b288a56da60801f7ee/yarl-1.25.1-cp315-cp315t-win_amd64.whl", hash = "sha256:b13b88747769537f3d32e89e3a735da10c0a9e35d7322928c701b5f93d3afffd", size = 106811, upload-time = "2026-09-15T19:34:54.935Z" }, + { url = "https://files.pythonhosted.org/packages/88/91/41e284ca2cf5211e05dae031d126a3668aea88fa759df56e7e35c6ad25ba/yarl-1.25.1-cp315-cp315t-win_arm64.whl", hash = "sha256:783dd1467083f4d3f7722ad6a313f24c173e7571372738fcb7a6e6d1ba48df25", size = 101804, upload-time = "2026-09-15T19:34:57.231Z" }, + { url = "https://files.pythonhosted.org/packages/54/22/318c7980066769c6bcd9221ed2248294f5698811da099013098c670565ed/yarl-1.25.1-py3-none-any.whl", hash = "sha256:681c758b0490f9e96b78e5fa8e8dc6e648e9185bb6eaebe73183c33ea0c445f3", size = 63617, upload-time = "2026-09-15T19:34:59.616Z" }, +] From 9864a899408b59fbd8372d6cc88624d5a45cb6d5 Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommu Date: Mon, 5 Oct 2026 08:55:05 -0700 Subject: [PATCH 2/4] fix: name GET /connectors connectors.list_all list_providers was wrong: the route lists connectors across the org, not providers. Matches the TypeScript SDK (listAll) and the docs. --- MIGRATION.md | 2 +- README.md | 2 +- fern/overlay.yaml | 2 +- reference.md | 4 ++-- src/supermemory/connectors/client.py | 12 ++++++------ src/supermemory/connectors/raw_client.py | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 4043dd9f..ca5b6201 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -44,7 +44,7 @@ Only the API methods change. Everything around them works as before: | `client.memories.forget(...)` | `client.memories.forget(namespace, ids=[...])` | | — | `client.memories.forget_matching(...)` | | — | `client.profiles.{get_buckets, set_buckets, delete_buckets}` | -| `client.connections.*` | `client.connectors.{list_providers, list, create, get, update, delete, sync}` | +| `client.connections.*` | `client.connectors.{list_all, list, create, get, update, delete, sync}` | | container tag management | `client.namespaces.{list, get, update, delete}` | | `client.settings.{get, update}` | `client.organization.{get, update}` | diff --git a/README.md b/README.md index 1b677726..1abf6bdf 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Install `supermemory[aiohttp]` to use aiohttp as the async transport. | `client.documents.{get, update, delete, batch_add, upload_file, replace_with_file, update_file}` | `/ns/{namespace}/document…` | | `client.memories.{forget, forget_matching}` | `/ns/{namespace}/memories…` | | `client.profiles.{get_buckets, set_buckets, delete_buckets}` | `/ns/{namespace}/profile/buckets` | -| `client.connectors.{list_providers, list, create, get, update, delete, sync}` | `/connectors`, `/ns/{namespace}/connectors…` | +| `client.connectors.{list_all, list, create, get, update, delete, sync}` | `/connectors`, `/ns/{namespace}/connectors…` | | `client.namespaces.{list, get, update, delete}` | `/ns`, `/ns/{namespace}` | | `client.organization.{get, update}` | `/organization` | diff --git a/fern/overlay.yaml b/fern/overlay.yaml index 3a5af74d..3148c24b 100644 --- a/fern/overlay.yaml +++ b/fern/overlay.yaml @@ -26,7 +26,7 @@ actions: - { target: '$.paths["/ns/{namespace}/profile/buckets"].put', update: { x-fern-sdk-group-name: profiles, x-fern-sdk-method-name: set_buckets } } - { target: '$.paths["/ns/{namespace}/profile/buckets"].delete', update: { x-fern-sdk-group-name: profiles, x-fern-sdk-method-name: delete_buckets } } # connectors - - { target: '$.paths["/connectors"].get', update: { x-fern-sdk-group-name: connectors, x-fern-sdk-method-name: list_providers } } + - { target: '$.paths["/connectors"].get', update: { x-fern-sdk-group-name: connectors, x-fern-sdk-method-name: list_all } } - { target: '$.paths["/ns/{namespace}/connectors"].post', update: { x-fern-sdk-group-name: connectors, x-fern-sdk-method-name: create } } - { target: '$.paths["/ns/{namespace}/connectors"].get', update: { x-fern-sdk-group-name: connectors, x-fern-sdk-method-name: list } } - { target: '$.paths["/ns/{namespace}/connectors/{id}"].get', update: { x-fern-sdk-group-name: connectors, x-fern-sdk-method-name: get } } diff --git a/reference.md b/reference.md index 256bcf64..ba5c13f2 100644 --- a/reference.md +++ b/reference.md @@ -1777,7 +1777,7 @@ client.memories.forget_matching( ## Connectors -
client.connectors.list_providers(...) -> ConnectorList +
client.connectors.list_all(...) -> ConnectorList
@@ -1812,7 +1812,7 @@ client = Supermemory( environment=SupermemoryEnvironment.DEFAULT, ) -client.connectors.list_providers() +client.connectors.list_all() ```
diff --git a/src/supermemory/connectors/client.py b/src/supermemory/connectors/client.py index f1d8074c..5b3ec60f 100644 --- a/src/supermemory/connectors/client.py +++ b/src/supermemory/connectors/client.py @@ -36,7 +36,7 @@ def with_raw_response(self) -> RawConnectorsClient: """ return self._raw_client - def list_providers( + def list_all( self, *, provider: typing.Optional[ConnectorProvider] = None, @@ -77,9 +77,9 @@ def list_providers( client = Supermemory( api_key="YOUR_API_KEY", ) - client.connectors.list_providers() + client.connectors.list_all() """ - _response = self._raw_client.list_providers( + _response = self._raw_client.list_all( provider=provider, page=page, limit=limit, request_options=request_options ) return _response.data @@ -404,7 +404,7 @@ def with_raw_response(self) -> AsyncRawConnectorsClient: """ return self._raw_client - async def list_providers( + async def list_all( self, *, provider: typing.Optional[ConnectorProvider] = None, @@ -450,12 +450,12 @@ async def list_providers( async def main() -> None: - await client.connectors.list_providers() + await client.connectors.list_all() asyncio.run(main()) """ - _response = await self._raw_client.list_providers( + _response = await self._raw_client.list_all( provider=provider, page=page, limit=limit, request_options=request_options ) return _response.data diff --git a/src/supermemory/connectors/raw_client.py b/src/supermemory/connectors/raw_client.py index 84929b9c..8f2655eb 100644 --- a/src/supermemory/connectors/raw_client.py +++ b/src/supermemory/connectors/raw_client.py @@ -42,7 +42,7 @@ class RawConnectorsClient: def __init__(self, *, client_wrapper: SyncClientWrapper): self._client_wrapper = client_wrapper - def list_providers( + def list_all( self, *, provider: typing.Optional[ConnectorProvider] = None, @@ -897,7 +897,7 @@ class AsyncRawConnectorsClient: def __init__(self, *, client_wrapper: AsyncClientWrapper): self._client_wrapper = client_wrapper - async def list_providers( + async def list_all( self, *, provider: typing.Optional[ConnectorProvider] = None, From 5d420ef215f3786eb8023fb4e6d940c701523f7a Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommu Date: Mon, 5 Oct 2026 10:45:39 -0700 Subject: [PATCH 3/4] fix: keep httpx.Timeout settings and retry timeouts like the 3.x client The compat layer handed Fern only timeout.read, so a per-request float replaced the client's httpx.Timeout and dropped connect/write/pool. It now forwards the whole Timeout object, which Fern passes straight to httpx. Fern's HTTP client only retried ConnectError and RemoteProtocolError; a new patch adds httpx.TimeoutException so read timeouts honor max_retries as before. --- custom/patches/0003-retry-timeouts.patch | 21 ++++++++++++++++++ custom/supermemory/_compat.py | 6 ++--- src/supermemory/_compat.py | 6 ++--- src/supermemory/core/http_client.py | 4 ++-- tests/test_compat.py | 28 ++++++++++++++++++++++++ 5 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 custom/patches/0003-retry-timeouts.patch diff --git a/custom/patches/0003-retry-timeouts.patch b/custom/patches/0003-retry-timeouts.patch new file mode 100644 index 00000000..3208528f --- /dev/null +++ b/custom/patches/0003-retry-timeouts.patch @@ -0,0 +1,21 @@ +diff -ru a/src/supermemory/core/http_client.py b/src/supermemory/core/http_client.py +--- a/src/supermemory/core/http_client.py ++++ b/src/supermemory/core/http_client.py +@@ -392,7 +392,7 @@ + files=request_files, + timeout=timeout, + ) +- except (httpx.ConnectError, httpx.RemoteProtocolError): ++ except (httpx.ConnectError, httpx.RemoteProtocolError, httpx.TimeoutException): + if retries < max_retries: + time.sleep(_retry_timeout_from_retries(retries=retries)) + return self.request( +@@ -690,7 +690,7 @@ + files=request_files, + timeout=timeout, + ) +- except (httpx.ConnectError, httpx.RemoteProtocolError): ++ except (httpx.ConnectError, httpx.RemoteProtocolError, httpx.TimeoutException): + if retries < max_retries: + await asyncio.sleep(_retry_timeout_from_retries(retries=retries)) + return await self.request( diff --git a/custom/supermemory/_compat.py b/custom/supermemory/_compat.py index a9921870..0068a2c5 100644 --- a/custom/supermemory/_compat.py +++ b/custom/supermemory/_compat.py @@ -256,7 +256,7 @@ def _apply_per_call_options(kwargs: dict[str, Any], defaults: _Defaults) -> None **request_options.get("additional_body_parameters", {}), } if timeout is not None: - request_options["timeout"] = timeout.read if isinstance(timeout, httpx.Timeout) else timeout + request_options["timeout"] = timeout # httpx.Timeout keeps connect/write/pool; Fern just forwards it kwargs["request_options"] = request_options @@ -404,7 +404,7 @@ def __init__( super().__init__( api_key=self.api_key, base_url=self.base_url, - timeout=self.timeout.read, + timeout=cast(float, self.timeout), # whole httpx.Timeout, forwarded as-is max_retries=max_retries, headers=headers, httpx_client=self._http_client, @@ -489,7 +489,7 @@ def __init__( super().__init__( api_key=self.api_key, base_url=self.base_url, - timeout=self.timeout.read, + timeout=cast(float, self.timeout), # whole httpx.Timeout, forwarded as-is max_retries=max_retries, headers=headers, httpx_client=self._http_client, diff --git a/src/supermemory/_compat.py b/src/supermemory/_compat.py index a9921870..0068a2c5 100644 --- a/src/supermemory/_compat.py +++ b/src/supermemory/_compat.py @@ -256,7 +256,7 @@ def _apply_per_call_options(kwargs: dict[str, Any], defaults: _Defaults) -> None **request_options.get("additional_body_parameters", {}), } if timeout is not None: - request_options["timeout"] = timeout.read if isinstance(timeout, httpx.Timeout) else timeout + request_options["timeout"] = timeout # httpx.Timeout keeps connect/write/pool; Fern just forwards it kwargs["request_options"] = request_options @@ -404,7 +404,7 @@ def __init__( super().__init__( api_key=self.api_key, base_url=self.base_url, - timeout=self.timeout.read, + timeout=cast(float, self.timeout), # whole httpx.Timeout, forwarded as-is max_retries=max_retries, headers=headers, httpx_client=self._http_client, @@ -489,7 +489,7 @@ def __init__( super().__init__( api_key=self.api_key, base_url=self.base_url, - timeout=self.timeout.read, + timeout=cast(float, self.timeout), # whole httpx.Timeout, forwarded as-is max_retries=max_retries, headers=headers, httpx_client=self._http_client, diff --git a/src/supermemory/core/http_client.py b/src/supermemory/core/http_client.py index 124dce25..cfe7684f 100644 --- a/src/supermemory/core/http_client.py +++ b/src/supermemory/core/http_client.py @@ -392,7 +392,7 @@ def request( files=request_files, timeout=timeout, ) - except (httpx.ConnectError, httpx.RemoteProtocolError): + except (httpx.ConnectError, httpx.RemoteProtocolError, httpx.TimeoutException): if retries < max_retries: time.sleep(_retry_timeout_from_retries(retries=retries)) return self.request( @@ -690,7 +690,7 @@ async def request( files=request_files, timeout=timeout, ) - except (httpx.ConnectError, httpx.RemoteProtocolError): + except (httpx.ConnectError, httpx.RemoteProtocolError, httpx.TimeoutException): if retries < max_retries: await asyncio.sleep(_retry_timeout_from_retries(retries=retries)) return await self.request( diff --git a/tests/test_compat.py b/tests/test_compat.py index e46bc490..b537de67 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -133,6 +133,34 @@ def handle(request: httpx.Request) -> httpx.Response: assert exc.value.request is not None +def test_timeout_object_is_forwarded_whole() -> None: + captured: list[httpx.Request] = [] + client = _client(_ok(captured, []), timeout=httpx.Timeout(10.0, connect=2.5)) + client.namespaces.list() + assert captured[0].extensions["timeout"] == {"connect": 2.5, "read": 10.0, "write": 10.0, "pool": 10.0} + + client.namespaces.list(timeout=httpx.Timeout(20.0, connect=1.0)) + assert captured[1].extensions["timeout"]["connect"] == 1.0 + assert captured[1].extensions["timeout"]["read"] == 20.0 + + +def test_read_timeout_is_retried(monkeypatch: pytest.MonkeyPatch) -> None: + import supermemory.core.http_client as http_client + + monkeypatch.setattr(http_client.time, "sleep", lambda _: None) + calls = 0 + + def handle(request: httpx.Request) -> httpx.Response: + nonlocal calls + calls += 1 + if calls == 1: + raise httpx.ReadTimeout("slow", request=request) + return httpx.Response(200, json=[]) + + assert _client(handle, max_retries=2).namespaces.list() == [] + assert calls == 2 + + def test_connection_error() -> None: def handle(request: httpx.Request) -> httpx.Response: raise httpx.ConnectError("refused", request=request) From 392384961ba9767b998c2efda99d45d7880ae1a6 Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommu Date: Mon, 5 Oct 2026 10:56:41 -0700 Subject: [PATCH 4/4] fix: timeout=None disables timeouts, as in the 3.x client Brings back the NOT_GIVEN sentinel so "not supplied" and an explicit None stay distinct. Omitted keeps the default (60 s read, 5 s connect); None at construction, per call, or via with_options turns all timeout phases off. NOT_GIVEN and NotGiven are exported again. --- custom/supermemory/_compat.py | 30 ++++++++++++---------- custom/supermemory/_types.py | 19 ++++++++++++++ scripts/apply_custom.py | 9 ++++++- src/supermemory/_compat.py | 30 ++++++++++++---------- src/supermemory/_types.py | 19 ++++++++++++++ src/supermemory/client.py | 18 +++++++------ src/supermemory/connectors/client.py | 30 ++++++++++++---------- src/supermemory/connectors/raw_client.py | 30 ++++++++++++---------- src/supermemory/documents/client.py | 30 ++++++++++++---------- src/supermemory/documents/raw_client.py | 30 ++++++++++++---------- src/supermemory/memories/client.py | 10 +++++--- src/supermemory/memories/raw_client.py | 10 +++++--- src/supermemory/namespaces/client.py | 18 +++++++------ src/supermemory/namespaces/raw_client.py | 18 +++++++------ src/supermemory/organization/client.py | 10 +++++--- src/supermemory/organization/raw_client.py | 10 +++++--- src/supermemory/profiles/client.py | 14 +++++----- src/supermemory/profiles/raw_client.py | 14 +++++----- src/supermemory/raw_client.py | 18 +++++++------ tests/test_compat.py | 21 +++++++++++++++ 20 files changed, 243 insertions(+), 145 deletions(-) create mode 100644 custom/supermemory/_types.py create mode 100644 src/supermemory/_types.py diff --git a/custom/supermemory/_compat.py b/custom/supermemory/_compat.py index 0068a2c5..8d4f436c 100644 --- a/custom/supermemory/_compat.py +++ b/custom/supermemory/_compat.py @@ -30,6 +30,7 @@ import pydantic from typing_extensions import Self +from ._types import NOT_GIVEN, NotGiven from .client import AsyncSupermemory as _GeneratedAsyncSupermemory from .client import Supermemory as _GeneratedSupermemory from .core.api_error import ApiError @@ -38,6 +39,8 @@ __all__ = [ "DEFAULT_MAX_RETRIES", "DEFAULT_TIMEOUT", + "NOT_GIVEN", + "NotGiven", "APIConnectionError", "APIError", "APIResponseValidationError", @@ -238,9 +241,9 @@ def _apply_per_call_options(kwargs: dict[str, Any], defaults: _Defaults) -> None extra_headers = kwargs.pop("extra_headers", None) extra_query = kwargs.pop("extra_query", None) extra_body = kwargs.pop("extra_body", None) - timeout = kwargs.pop("timeout", None) + timeout = kwargs.pop("timeout", NOT_GIVEN) query = {**defaults.default_query, **(extra_query or {})} - if not (extra_headers or query or extra_body or timeout is not None): + if not (extra_headers or query or extra_body or not isinstance(timeout, NotGiven)): return request_options: dict[str, Any] = dict(kwargs.get("request_options") or {}) if extra_headers: @@ -255,8 +258,9 @@ def _apply_per_call_options(kwargs: dict[str, Any], defaults: _Defaults) -> None **extra_body, **request_options.get("additional_body_parameters", {}), } - if timeout is not None: - request_options["timeout"] = timeout # httpx.Timeout keeps connect/write/pool; Fern just forwards it + if not isinstance(timeout, NotGiven): + # httpx.Timeout keeps connect/write/pool; Fern just forwards it. None disables timeouts, as in 3.x. + request_options["timeout"] = httpx.Timeout(None) if timeout is None else timeout kwargs["request_options"] = request_options @@ -347,14 +351,12 @@ def _resolve(api_key: str | None, base_url: str | httpx.URL | None) -> tuple[str return api_key, str(base_url or "https://api.supermemory.ai").rstrip("/") -def _resolve_timeout(timeout: float | httpx.Timeout | None) -> httpx.Timeout: - return ( - timeout - if isinstance(timeout, httpx.Timeout) - else httpx.Timeout(timeout) - if timeout is not None - else DEFAULT_TIMEOUT - ) +def _resolve_timeout(timeout: float | httpx.Timeout | None | NotGiven) -> httpx.Timeout: + if isinstance(timeout, NotGiven): + return DEFAULT_TIMEOUT + if timeout is None: + return httpx.Timeout(None) # 3.x: explicit None disables timeouts + return timeout if isinstance(timeout, httpx.Timeout) else httpx.Timeout(timeout) def _add_hook(client: httpx.Client | httpx.AsyncClient, hook: Callable[..., Any]) -> None: @@ -375,7 +377,7 @@ def __init__( *, api_key: str | None = None, base_url: str | httpx.URL | None = None, - timeout: float | httpx.Timeout | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, default_query: Mapping[str, object] | None = None, @@ -460,7 +462,7 @@ def __init__( *, api_key: str | None = None, base_url: str | httpx.URL | None = None, - timeout: float | httpx.Timeout | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, default_query: Mapping[str, object] | None = None, diff --git a/custom/supermemory/_types.py b/custom/supermemory/_types.py new file mode 100644 index 00000000..50002b13 --- /dev/null +++ b/custom/supermemory/_types.py @@ -0,0 +1,19 @@ +"""The 3.x `NOT_GIVEN` sentinel: "option not supplied", distinct from an explicit `None`.""" + +from __future__ import annotations + +from typing import Literal + + +class NotGiven: + def __bool__(self) -> Literal[False]: + return False + + def __repr__(self) -> str: + return "NOT_GIVEN" + + +NOT_GIVEN = NotGiven() +not_given = NOT_GIVEN + +__all__ = ["NOT_GIVEN", "NotGiven", "not_given"] diff --git a/scripts/apply_custom.py b/scripts/apply_custom.py index 833164a6..b192c95f 100644 --- a/scripts/apply_custom.py +++ b/scripts/apply_custom.py @@ -34,7 +34,7 @@ def compat_exports() -> set[str]: ", extra_headers: typing.Optional[typing.Mapping[str, str]] = None" ", extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None" ", extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None" - ", timeout: typing.Union[float, httpx.Timeout, None] = None" + ", timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN" ) PER_CALL_NAMES = {"extra_headers", "extra_query", "extra_body", "timeout"} @@ -65,6 +65,13 @@ def add_per_call_options(path: Path) -> None: source = "".join(lines) if not re.search(r"^import httpx$", source, re.M): source = re.sub(r"^import typing$", "import typing\n\nimport httpx", source, count=1, flags=re.M) + source = re.sub( + r"^import httpx$", + "import httpx\n\nfrom supermemory._types import NOT_GIVEN, NotGiven", + source, + count=1, + flags=re.M, + ) path.write_text(source) diff --git a/src/supermemory/_compat.py b/src/supermemory/_compat.py index 0068a2c5..8d4f436c 100644 --- a/src/supermemory/_compat.py +++ b/src/supermemory/_compat.py @@ -30,6 +30,7 @@ import pydantic from typing_extensions import Self +from ._types import NOT_GIVEN, NotGiven from .client import AsyncSupermemory as _GeneratedAsyncSupermemory from .client import Supermemory as _GeneratedSupermemory from .core.api_error import ApiError @@ -38,6 +39,8 @@ __all__ = [ "DEFAULT_MAX_RETRIES", "DEFAULT_TIMEOUT", + "NOT_GIVEN", + "NotGiven", "APIConnectionError", "APIError", "APIResponseValidationError", @@ -238,9 +241,9 @@ def _apply_per_call_options(kwargs: dict[str, Any], defaults: _Defaults) -> None extra_headers = kwargs.pop("extra_headers", None) extra_query = kwargs.pop("extra_query", None) extra_body = kwargs.pop("extra_body", None) - timeout = kwargs.pop("timeout", None) + timeout = kwargs.pop("timeout", NOT_GIVEN) query = {**defaults.default_query, **(extra_query or {})} - if not (extra_headers or query or extra_body or timeout is not None): + if not (extra_headers or query or extra_body or not isinstance(timeout, NotGiven)): return request_options: dict[str, Any] = dict(kwargs.get("request_options") or {}) if extra_headers: @@ -255,8 +258,9 @@ def _apply_per_call_options(kwargs: dict[str, Any], defaults: _Defaults) -> None **extra_body, **request_options.get("additional_body_parameters", {}), } - if timeout is not None: - request_options["timeout"] = timeout # httpx.Timeout keeps connect/write/pool; Fern just forwards it + if not isinstance(timeout, NotGiven): + # httpx.Timeout keeps connect/write/pool; Fern just forwards it. None disables timeouts, as in 3.x. + request_options["timeout"] = httpx.Timeout(None) if timeout is None else timeout kwargs["request_options"] = request_options @@ -347,14 +351,12 @@ def _resolve(api_key: str | None, base_url: str | httpx.URL | None) -> tuple[str return api_key, str(base_url or "https://api.supermemory.ai").rstrip("/") -def _resolve_timeout(timeout: float | httpx.Timeout | None) -> httpx.Timeout: - return ( - timeout - if isinstance(timeout, httpx.Timeout) - else httpx.Timeout(timeout) - if timeout is not None - else DEFAULT_TIMEOUT - ) +def _resolve_timeout(timeout: float | httpx.Timeout | None | NotGiven) -> httpx.Timeout: + if isinstance(timeout, NotGiven): + return DEFAULT_TIMEOUT + if timeout is None: + return httpx.Timeout(None) # 3.x: explicit None disables timeouts + return timeout if isinstance(timeout, httpx.Timeout) else httpx.Timeout(timeout) def _add_hook(client: httpx.Client | httpx.AsyncClient, hook: Callable[..., Any]) -> None: @@ -375,7 +377,7 @@ def __init__( *, api_key: str | None = None, base_url: str | httpx.URL | None = None, - timeout: float | httpx.Timeout | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, default_query: Mapping[str, object] | None = None, @@ -460,7 +462,7 @@ def __init__( *, api_key: str | None = None, base_url: str | httpx.URL | None = None, - timeout: float | httpx.Timeout | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, default_query: Mapping[str, object] | None = None, diff --git a/src/supermemory/_types.py b/src/supermemory/_types.py new file mode 100644 index 00000000..50002b13 --- /dev/null +++ b/src/supermemory/_types.py @@ -0,0 +1,19 @@ +"""The 3.x `NOT_GIVEN` sentinel: "option not supplied", distinct from an explicit `None`.""" + +from __future__ import annotations + +from typing import Literal + + +class NotGiven: + def __bool__(self) -> Literal[False]: + return False + + def __repr__(self) -> str: + return "NOT_GIVEN" + + +NOT_GIVEN = NotGiven() +not_given = NOT_GIVEN + +__all__ = ["NOT_GIVEN", "NotGiven", "not_given"] diff --git a/src/supermemory/client.py b/src/supermemory/client.py index 875551dc..7999535f 100644 --- a/src/supermemory/client.py +++ b/src/supermemory/client.py @@ -6,6 +6,8 @@ import typing import httpx + +from supermemory._types import NOT_GIVEN, NotGiven from .core.api_error import ApiError from .core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from .core.logging import LogConfig, Logger @@ -163,7 +165,7 @@ def add( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> DocumentRef: """ Turn text or a supported URL into searchable, evolving memory. Supply a new ID to create a document, or reuse an existing ID to append new information while preserving its history. @@ -247,7 +249,7 @@ def search( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> SearchResponse: """ Recall the most relevant learned context and source passages from a namespace. Hybrid search combines memories with document chunks by default, with optional query rewriting, reranking, and supporting context attachments. @@ -326,7 +328,7 @@ def profile( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> ProfileResponse: """ Read a continuously maintained understanding of the subject represented by this namespace. Stable facts, evolving context, and selected custom buckets are returned together without requiring a search query. @@ -378,7 +380,7 @@ def list( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> ListResponse: """ Browse documents, source chunks, or learned memories through one predictable paginated contract. Choose the collection in the path; the other collection arrays remain empty for a stable response shape. @@ -631,7 +633,7 @@ async def add( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> DocumentRef: """ Turn text or a supported URL into searchable, evolving memory. Supply a new ID to create a document, or reuse an existing ID to append new information while preserving its history. @@ -723,7 +725,7 @@ async def search( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> SearchResponse: """ Recall the most relevant learned context and source passages from a namespace. Hybrid search combines memories with document chunks by default, with optional query rewriting, reranking, and supporting context attachments. @@ -810,7 +812,7 @@ async def profile( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> ProfileResponse: """ Read a continuously maintained understanding of the subject represented by this namespace. Stable facts, evolving context, and selected custom buckets are returned together without requiring a search query. @@ -872,7 +874,7 @@ async def list( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> ListResponse: """ Browse documents, source chunks, or learned memories through one predictable paginated contract. Choose the collection in the path; the other collection arrays remain empty for a stable response shape. diff --git a/src/supermemory/connectors/client.py b/src/supermemory/connectors/client.py index 5b3ec60f..fc54ffbf 100644 --- a/src/supermemory/connectors/client.py +++ b/src/supermemory/connectors/client.py @@ -4,6 +4,8 @@ import httpx +from supermemory._types import NOT_GIVEN, NotGiven + from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from ..core.request_options import RequestOptions from ..requests.connector_selection import ConnectorSelectionParams @@ -46,7 +48,7 @@ def list_all( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> ConnectorList: """ List connectors across every namespace this key can read. Use it for admin views that span users or projects. @@ -95,7 +97,7 @@ def list( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> ConnectorList: """ List the connectors that sync into this namespace. @@ -147,7 +149,7 @@ def create( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> ConnectorSetupResult: """ Connect an external source to this namespace. OAuth providers return an authUrl to send the user to; providers that authenticate with config start syncing right away. @@ -193,7 +195,7 @@ def get( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> Connector: """ Read one connector. Add attach=syncs to include its recent sync runs and the items that failed. attach=picker needs an admin with write access, since the link changes what syncs. @@ -247,7 +249,7 @@ def delete( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> ConnectorDeleted: """ Disconnect a connector and stop its webhooks. Its imported documents are deleted too unless deleteDocuments is false. @@ -299,7 +301,7 @@ def update( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> Connector: """ Change what a connector syncs. A new selection replaces the old one and starts a sync. @@ -352,7 +354,7 @@ def sync( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> ConnectorSyncStarted: """ Start a sync now. Returns 409 while a sync for this connector is already running. @@ -414,7 +416,7 @@ async def list_all( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> ConnectorList: """ List connectors across every namespace this key can read. Use it for admin views that span users or projects. @@ -471,7 +473,7 @@ async def list( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> ConnectorList: """ List the connectors that sync into this namespace. @@ -531,7 +533,7 @@ async def create( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> ConnectorSetupResult: """ Connect an external source to this namespace. OAuth providers return an authUrl to send the user to; providers that authenticate with config start syncing right away. @@ -585,7 +587,7 @@ async def get( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> Connector: """ Read one connector. Add attach=syncs to include its recent sync runs and the items that failed. attach=picker needs an admin with write access, since the link changes what syncs. @@ -647,7 +649,7 @@ async def delete( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> ConnectorDeleted: """ Disconnect a connector and stop its webhooks. Its imported documents are deleted too unless deleteDocuments is false. @@ -707,7 +709,7 @@ async def update( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> Connector: """ Change what a connector syncs. A new selection replaces the old one and starts a sync. @@ -768,7 +770,7 @@ async def sync( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> ConnectorSyncStarted: """ Start a sync now. Returns 409 while a sync for this connector is already running. diff --git a/src/supermemory/connectors/raw_client.py b/src/supermemory/connectors/raw_client.py index 8f2655eb..2395f157 100644 --- a/src/supermemory/connectors/raw_client.py +++ b/src/supermemory/connectors/raw_client.py @@ -3,6 +3,8 @@ import typing import httpx + +from supermemory._types import NOT_GIVEN, NotGiven from json.decoder import JSONDecodeError from ..core.api_error import ApiError @@ -52,7 +54,7 @@ def list_all( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[ConnectorList]: """ List connectors across every namespace this key can read. Use it for admin views that span users or projects. @@ -160,7 +162,7 @@ def list( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[ConnectorList]: """ List the connectors that sync into this namespace. @@ -269,7 +271,7 @@ def create( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[ConnectorSetupResult]: """ Connect an external source to this namespace. OAuth providers return an authUrl to send the user to; providers that authenticate with config start syncing right away. @@ -397,7 +399,7 @@ def get( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[Connector]: """ Read one connector. Add attach=syncs to include its recent sync runs and the items that failed. attach=picker needs an admin with write access, since the link changes what syncs. @@ -517,7 +519,7 @@ def delete( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[ConnectorDeleted]: """ Disconnect a connector and stop its webhooks. Its imported documents are deleted too unless deleteDocuments is false. @@ -634,7 +636,7 @@ def update( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[Connector]: """ Change what a connector syncs. A new selection replaces the old one and starts a sync. @@ -759,7 +761,7 @@ def sync( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[ConnectorSyncStarted]: """ Start a sync now. Returns 409 while a sync for this connector is already running. @@ -907,7 +909,7 @@ async def list_all( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[ConnectorList]: """ List connectors across every namespace this key can read. Use it for admin views that span users or projects. @@ -1015,7 +1017,7 @@ async def list( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[ConnectorList]: """ List the connectors that sync into this namespace. @@ -1124,7 +1126,7 @@ async def create( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[ConnectorSetupResult]: """ Connect an external source to this namespace. OAuth providers return an authUrl to send the user to; providers that authenticate with config start syncing right away. @@ -1252,7 +1254,7 @@ async def get( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[Connector]: """ Read one connector. Add attach=syncs to include its recent sync runs and the items that failed. attach=picker needs an admin with write access, since the link changes what syncs. @@ -1372,7 +1374,7 @@ async def delete( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[ConnectorDeleted]: """ Disconnect a connector and stop its webhooks. Its imported documents are deleted too unless deleteDocuments is false. @@ -1489,7 +1491,7 @@ async def update( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[Connector]: """ Change what a connector syncs. A new selection replaces the old one and starts a sync. @@ -1614,7 +1616,7 @@ async def sync( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[ConnectorSyncStarted]: """ Start a sync now. Returns 409 while a sync for this connector is already running. diff --git a/src/supermemory/documents/client.py b/src/supermemory/documents/client.py index 7b9a1532..e5c85346 100644 --- a/src/supermemory/documents/client.py +++ b/src/supermemory/documents/client.py @@ -4,6 +4,8 @@ import httpx +from supermemory._types import NOT_GIVEN, NotGiven + from .. import core from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from ..core.request_options import RequestOptions @@ -48,7 +50,7 @@ def delete( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> DeleteDocumentsResult: """ Permanently remove documents and their derived knowledge by document ID or caller-defined ID. Each requested ID is handled independently so successful deletions are preserved when another ID fails. @@ -95,7 +97,7 @@ def batch_add( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> BatchAddResult: """ Build a knowledge base efficiently by ingesting up to 600 text or URL documents at once. Existing caller-defined IDs append new information using the same semantics as single-document ingestion. @@ -153,7 +155,7 @@ def get( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> Document: """ Retrieve a document's canonical content, metadata, and processing state by document ID or caller-defined ID. Optionally attach its source chunks, derived memories, or both in the same response. @@ -208,7 +210,7 @@ def update( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> DocumentRef: """ Refresh an existing document without changing its stable ID. Supplied content replaces the canonical content and is reprocessed; omitted fields remain unchanged. @@ -293,7 +295,7 @@ def upload_file( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> FileUploadResult: """ Transform an uploaded file into searchable knowledge and learned memory. The response returns as soon as ingestion is safely queued while extraction and memory formation continue asynchronously. @@ -382,7 +384,7 @@ def replace_with_file( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> FileUploadResult: """ Replace an existing document with a new file while keeping its stable document ID. Content and caller metadata are overwritten, then the document is reprocessed asynchronously. @@ -476,7 +478,7 @@ def update_file( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> FileUploadResult: """ Refresh only the file-backed fields you provide. Supplying a file replaces the canonical content; omitted metadata and processing context remain unchanged. @@ -577,7 +579,7 @@ async def delete( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> DeleteDocumentsResult: """ Permanently remove documents and their derived knowledge by document ID or caller-defined ID. Each requested ID is handled independently so successful deletions are preserved when another ID fails. @@ -632,7 +634,7 @@ async def batch_add( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> BatchAddResult: """ Build a knowledge base efficiently by ingesting up to 600 text or URL documents at once. Existing caller-defined IDs append new information using the same semantics as single-document ingestion. @@ -698,7 +700,7 @@ async def get( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> Document: """ Retrieve a document's canonical content, metadata, and processing state by document ID or caller-defined ID. Optionally attach its source chunks, derived memories, or both in the same response. @@ -761,7 +763,7 @@ async def update( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> DocumentRef: """ Refresh an existing document without changing its stable ID. Supplied content replaces the canonical content and is reprocessed; omitted fields remain unchanged. @@ -854,7 +856,7 @@ async def upload_file( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> FileUploadResult: """ Transform an uploaded file into searchable knowledge and learned memory. The response returns as soon as ingestion is safely queued while extraction and memory formation continue asynchronously. @@ -951,7 +953,7 @@ async def replace_with_file( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> FileUploadResult: """ Replace an existing document with a new file while keeping its stable document ID. Content and caller metadata are overwritten, then the document is reprocessed asynchronously. @@ -1053,7 +1055,7 @@ async def update_file( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> FileUploadResult: """ Refresh only the file-backed fields you provide. Supplying a file replaces the canonical content; omitted metadata and processing context remain unchanged. diff --git a/src/supermemory/documents/raw_client.py b/src/supermemory/documents/raw_client.py index e81caed4..1a47265f 100644 --- a/src/supermemory/documents/raw_client.py +++ b/src/supermemory/documents/raw_client.py @@ -3,6 +3,8 @@ import typing import httpx + +from supermemory._types import NOT_GIVEN, NotGiven from json.decoder import JSONDecodeError from .. import core @@ -53,7 +55,7 @@ def delete( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[DeleteDocumentsResult]: """ Permanently remove documents and their derived knowledge by document ID or caller-defined ID. Each requested ID is handled independently so successful deletions are preserved when another ID fails. @@ -160,7 +162,7 @@ def batch_add( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[BatchAddResult]: """ Build a knowledge base efficiently by ingesting up to 600 text or URL documents at once. Existing caller-defined IDs append new information using the same semantics as single-document ingestion. @@ -289,7 +291,7 @@ def get( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[Document]: """ Retrieve a document's canonical content, metadata, and processing state by document ID or caller-defined ID. Optionally attach its source chunks, derived memories, or both in the same response. @@ -411,7 +413,7 @@ def update( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[DocumentRef]: """ Refresh an existing document without changing its stable ID. Supplied content replaces the canonical content and is reprocessed; omitted fields remain unchanged. @@ -590,7 +592,7 @@ def upload_file( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[FileUploadResult]: """ Transform an uploaded file into searchable knowledge and learned memory. The response returns as soon as ingestion is safely queued while extraction and memory formation continue asynchronously. @@ -760,7 +762,7 @@ def replace_with_file( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[FileUploadResult]: """ Replace an existing document with a new file while keeping its stable document ID. Content and caller metadata are overwritten, then the document is reprocessed asynchronously. @@ -944,7 +946,7 @@ def update_file( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[FileUploadResult]: """ Refresh only the file-backed fields you provide. Supplying a file replaces the canonical content; omitted metadata and processing context remain unchanged. @@ -1124,7 +1126,7 @@ async def delete( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[DeleteDocumentsResult]: """ Permanently remove documents and their derived knowledge by document ID or caller-defined ID. Each requested ID is handled independently so successful deletions are preserved when another ID fails. @@ -1231,7 +1233,7 @@ async def batch_add( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[BatchAddResult]: """ Build a knowledge base efficiently by ingesting up to 600 text or URL documents at once. Existing caller-defined IDs append new information using the same semantics as single-document ingestion. @@ -1360,7 +1362,7 @@ async def get( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[Document]: """ Retrieve a document's canonical content, metadata, and processing state by document ID or caller-defined ID. Optionally attach its source chunks, derived memories, or both in the same response. @@ -1482,7 +1484,7 @@ async def update( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[DocumentRef]: """ Refresh an existing document without changing its stable ID. Supplied content replaces the canonical content and is reprocessed; omitted fields remain unchanged. @@ -1661,7 +1663,7 @@ async def upload_file( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[FileUploadResult]: """ Transform an uploaded file into searchable knowledge and learned memory. The response returns as soon as ingestion is safely queued while extraction and memory formation continue asynchronously. @@ -1831,7 +1833,7 @@ async def replace_with_file( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[FileUploadResult]: """ Replace an existing document with a new file while keeping its stable document ID. Content and caller metadata are overwritten, then the document is reprocessed asynchronously. @@ -2015,7 +2017,7 @@ async def update_file( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[FileUploadResult]: """ Refresh only the file-backed fields you provide. Supplying a file replaces the canonical content; omitted metadata and processing context remain unchanged. diff --git a/src/supermemory/memories/client.py b/src/supermemory/memories/client.py index 6b4c406f..875f4535 100644 --- a/src/supermemory/memories/client.py +++ b/src/supermemory/memories/client.py @@ -4,6 +4,8 @@ import httpx +from supermemory._types import NOT_GIVEN, NotGiven + from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from ..core.request_options import RequestOptions from ..types.forget_result import ForgetResult @@ -37,7 +39,7 @@ def forget( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> ForgetResult: """ Remove exact memories from normal recall while preserving their audit history. Each ID is handled independently and any missing or ineligible memory is reported without rolling back successful changes. @@ -83,7 +85,7 @@ def forget_matching( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> ForgetResult: """ Describe what should be forgotten in natural language, then preview or apply the matching set. Use dry-run results with the exact-ID endpoint when you need a reviewable, drift-free workflow. @@ -150,7 +152,7 @@ async def forget( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> ForgetResult: """ Remove exact memories from normal recall while preserving their audit history. Each ID is handled independently and any missing or ineligible memory is reported without rolling back successful changes. @@ -204,7 +206,7 @@ async def forget_matching( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> ForgetResult: """ Describe what should be forgotten in natural language, then preview or apply the matching set. Use dry-run results with the exact-ID endpoint when you need a reviewable, drift-free workflow. diff --git a/src/supermemory/memories/raw_client.py b/src/supermemory/memories/raw_client.py index b86352bf..7c8fa3ac 100644 --- a/src/supermemory/memories/raw_client.py +++ b/src/supermemory/memories/raw_client.py @@ -3,6 +3,8 @@ import typing import httpx + +from supermemory._types import NOT_GIVEN, NotGiven from json.decoder import JSONDecodeError from ..core.api_error import ApiError @@ -38,7 +40,7 @@ def forget( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[ForgetResult]: """ Remove exact memories from normal recall while preserving their audit history. Each ID is handled independently and any missing or ineligible memory is reported without rolling back successful changes. @@ -144,7 +146,7 @@ def forget_matching( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[ForgetResult]: """ Describe what should be forgotten in natural language, then preview or apply the matching set. Use dry-run results with the exact-ID endpoint when you need a reviewable, drift-free workflow. @@ -258,7 +260,7 @@ async def forget( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[ForgetResult]: """ Remove exact memories from normal recall while preserving their audit history. Each ID is handled independently and any missing or ineligible memory is reported without rolling back successful changes. @@ -364,7 +366,7 @@ async def forget_matching( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[ForgetResult]: """ Describe what should be forgotten in natural language, then preview or apply the matching set. Use dry-run results with the exact-ID endpoint when you need a reviewable, drift-free workflow. diff --git a/src/supermemory/namespaces/client.py b/src/supermemory/namespaces/client.py index d1dc0b3e..7768aaf0 100644 --- a/src/supermemory/namespaces/client.py +++ b/src/supermemory/namespaces/client.py @@ -4,6 +4,8 @@ import httpx +from supermemory._types import NOT_GIVEN, NotGiven + from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from ..core.request_options import RequestOptions from ..types.namespace import Namespace @@ -37,7 +39,7 @@ def list( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> typing.List[Namespace]: """ Discover every namespace available to the caller, including its purpose, document volume, memory count, and lifecycle timestamps. @@ -72,7 +74,7 @@ def get( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> NamespaceDetails: """ Read the supporting context that helps Supermemory understand content and form better memories inside this namespace. @@ -113,7 +115,7 @@ def delete( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> NamespaceDeleted: """ Retire a namespace by permanently deleting its content, or preserve that knowledge by moving everything into another namespace first. Moves are queued and complete asynchronously. @@ -157,7 +159,7 @@ def update( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> NamespaceDetails: """ Shape how Supermemory understands an existing namespace by updating the background context used during ingestion and memory formation. @@ -217,7 +219,7 @@ async def list( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> typing.List[Namespace]: """ Discover every namespace available to the caller, including its purpose, document volume, memory count, and lifecycle timestamps. @@ -260,7 +262,7 @@ async def get( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> NamespaceDetails: """ Read the supporting context that helps Supermemory understand content and form better memories inside this namespace. @@ -309,7 +311,7 @@ async def delete( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> NamespaceDeleted: """ Retire a namespace by permanently deleting its content, or preserve that knowledge by moving everything into another namespace first. Moves are queued and complete asynchronously. @@ -361,7 +363,7 @@ async def update( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> NamespaceDetails: """ Shape how Supermemory understands an existing namespace by updating the background context used during ingestion and memory formation. diff --git a/src/supermemory/namespaces/raw_client.py b/src/supermemory/namespaces/raw_client.py index 051d1c99..a298513f 100644 --- a/src/supermemory/namespaces/raw_client.py +++ b/src/supermemory/namespaces/raw_client.py @@ -3,6 +3,8 @@ import typing import httpx + +from supermemory._types import NOT_GIVEN, NotGiven from json.decoder import JSONDecodeError from ..core.api_error import ApiError @@ -40,7 +42,7 @@ def list( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[typing.List[Namespace]]: """ Discover every namespace available to the caller, including its purpose, document volume, memory count, and lifecycle timestamps. @@ -120,7 +122,7 @@ def get( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[NamespaceDetails]: """ Read the supporting context that helps Supermemory understand content and form better memories inside this namespace. @@ -226,7 +228,7 @@ def delete( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[NamespaceDeleted]: """ Retire a namespace by permanently deleting its content, or preserve that knowledge by moving everything into another namespace first. Moves are queued and complete asynchronously. @@ -353,7 +355,7 @@ def update( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[NamespaceDetails]: """ Shape how Supermemory understands an existing namespace by updating the background context used during ingestion and memory formation. @@ -472,7 +474,7 @@ async def list( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[typing.List[Namespace]]: """ Discover every namespace available to the caller, including its purpose, document volume, memory count, and lifecycle timestamps. @@ -552,7 +554,7 @@ async def get( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[NamespaceDetails]: """ Read the supporting context that helps Supermemory understand content and form better memories inside this namespace. @@ -658,7 +660,7 @@ async def delete( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[NamespaceDeleted]: """ Retire a namespace by permanently deleting its content, or preserve that knowledge by moving everything into another namespace first. Moves are queued and complete asynchronously. @@ -785,7 +787,7 @@ async def update( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[NamespaceDetails]: """ Shape how Supermemory understands an existing namespace by updating the background context used during ingestion and memory formation. diff --git a/src/supermemory/organization/client.py b/src/supermemory/organization/client.py index b45665c2..4bf200cd 100644 --- a/src/supermemory/organization/client.py +++ b/src/supermemory/organization/client.py @@ -4,6 +4,8 @@ import httpx +from supermemory._types import NOT_GIVEN, NotGiven + from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from ..core.request_options import RequestOptions from ..types.organization import Organization @@ -35,7 +37,7 @@ def get( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> Organization: """ Read the shared background that guides memory formation across the organization, together with the number of active namespaces. @@ -70,7 +72,7 @@ def update( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> Organization: """ Give Supermemory organization-wide background that improves how content is interpreted across every namespace. Send null to remove the existing context. @@ -125,7 +127,7 @@ async def get( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> Organization: """ Read the shared background that guides memory formation across the organization, together with the number of active namespaces. @@ -168,7 +170,7 @@ async def update( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> Organization: """ Give Supermemory organization-wide background that improves how content is interpreted across every namespace. Send null to remove the existing context. diff --git a/src/supermemory/organization/raw_client.py b/src/supermemory/organization/raw_client.py index aa64efef..2c4484ef 100644 --- a/src/supermemory/organization/raw_client.py +++ b/src/supermemory/organization/raw_client.py @@ -3,6 +3,8 @@ import typing import httpx + +from supermemory._types import NOT_GIVEN, NotGiven from json.decoder import JSONDecodeError from ..core.api_error import ApiError @@ -35,7 +37,7 @@ def get( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[Organization]: """ Read the shared background that guides memory formation across the organization, together with the number of active namespaces. @@ -115,7 +117,7 @@ def update( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[Organization]: """ Give Supermemory organization-wide background that improves how content is interpreted across every namespace. Send null to remove the existing context. @@ -220,7 +222,7 @@ async def get( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[Organization]: """ Read the shared background that guides memory formation across the organization, together with the number of active namespaces. @@ -300,7 +302,7 @@ async def update( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[Organization]: """ Give Supermemory organization-wide background that improves how content is interpreted across every namespace. Send null to remove the existing context. diff --git a/src/supermemory/profiles/client.py b/src/supermemory/profiles/client.py index fe52aab5..581b7939 100644 --- a/src/supermemory/profiles/client.py +++ b/src/supermemory/profiles/client.py @@ -4,6 +4,8 @@ import httpx +from supermemory._types import NOT_GIVEN, NotGiven + from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from ..core.request_options import RequestOptions from ..types.profile_buckets import ProfileBuckets @@ -36,7 +38,7 @@ def get_buckets( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> ProfileBuckets: """ Inspect the profile taxonomy active in this namespace. The response combines organization-wide buckets with namespace-owned additions as a concise name-to-description map. @@ -77,7 +79,7 @@ def set_buckets( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> ProfileBuckets: """ Teach Supermemory new ways to organize this namespace's profile, or refine how existing namespace-owned buckets are classified. Omitted buckets remain unchanged and organization-wide buckets stay protected. @@ -122,7 +124,7 @@ def delete_buckets( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> ProfileBuckets: """ Remove profile categories created specifically for this namespace. Unknown names are safely ignored, while organization-wide buckets remain protected. @@ -182,7 +184,7 @@ async def get_buckets( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> ProfileBuckets: """ Inspect the profile taxonomy active in this namespace. The response combines organization-wide buckets with namespace-owned additions as a concise name-to-description map. @@ -231,7 +233,7 @@ async def set_buckets( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> ProfileBuckets: """ Teach Supermemory new ways to organize this namespace's profile, or refine how existing namespace-owned buckets are classified. Omitted buckets remain unchanged and organization-wide buckets stay protected. @@ -284,7 +286,7 @@ async def delete_buckets( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> ProfileBuckets: """ Remove profile categories created specifically for this namespace. Unknown names are safely ignored, while organization-wide buckets remain protected. diff --git a/src/supermemory/profiles/raw_client.py b/src/supermemory/profiles/raw_client.py index 6774430d..8ec3423b 100644 --- a/src/supermemory/profiles/raw_client.py +++ b/src/supermemory/profiles/raw_client.py @@ -3,6 +3,8 @@ import typing import httpx + +from supermemory._types import NOT_GIVEN, NotGiven from json.decoder import JSONDecodeError from ..core.api_error import ApiError @@ -39,7 +41,7 @@ def get_buckets( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[ProfileBuckets]: """ Inspect the profile taxonomy active in this namespace. The response combines organization-wide buckets with namespace-owned additions as a concise name-to-description map. @@ -123,7 +125,7 @@ def set_buckets( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[ProfileBuckets]: """ Teach Supermemory new ways to organize this namespace's profile, or refine how existing namespace-owned buckets are classified. Omitted buckets remain unchanged and organization-wide buckets stay protected. @@ -250,7 +252,7 @@ def delete_buckets( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[ProfileBuckets]: """ Remove profile categories created specifically for this namespace. Unknown names are safely ignored, while organization-wide buckets remain protected. @@ -381,7 +383,7 @@ async def get_buckets( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[ProfileBuckets]: """ Inspect the profile taxonomy active in this namespace. The response combines organization-wide buckets with namespace-owned additions as a concise name-to-description map. @@ -465,7 +467,7 @@ async def set_buckets( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[ProfileBuckets]: """ Teach Supermemory new ways to organize this namespace's profile, or refine how existing namespace-owned buckets are classified. Omitted buckets remain unchanged and organization-wide buckets stay protected. @@ -592,7 +594,7 @@ async def delete_buckets( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[ProfileBuckets]: """ Remove profile categories created specifically for this namespace. Unknown names are safely ignored, while organization-wide buckets remain protected. diff --git a/src/supermemory/raw_client.py b/src/supermemory/raw_client.py index ea7aec98..6629d2ec 100644 --- a/src/supermemory/raw_client.py +++ b/src/supermemory/raw_client.py @@ -3,6 +3,8 @@ import typing import httpx + +from supermemory._types import NOT_GIVEN, NotGiven from json.decoder import JSONDecodeError from .core.api_error import ApiError @@ -63,7 +65,7 @@ def add( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[DocumentRef]: """ Turn text or a supported URL into searchable, evolving memory. Supply a new ID to create a document, or reuse an existing ID to append new information while preserving its history. @@ -231,7 +233,7 @@ def search( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[SearchResponse]: """ Recall the most relevant learned context and source passages from a namespace. Hybrid search combines memories with document chunks by default, with optional query rewriting, reranking, and supporting context attachments. @@ -382,7 +384,7 @@ def profile( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[ProfileResponse]: """ Read a continuously maintained understanding of the subject represented by this namespace. Stable facts, evolving context, and selected custom buckets are returned together without requiring a search query. @@ -498,7 +500,7 @@ def list( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> HttpResponse[ListResponse]: """ Browse documents, source chunks, or learned memories through one predictable paginated contract. Choose the collection in the path; the other collection arrays remain empty for a stable response shape. @@ -642,7 +644,7 @@ async def add( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[DocumentRef]: """ Turn text or a supported URL into searchable, evolving memory. Supply a new ID to create a document, or reuse an existing ID to append new information while preserving its history. @@ -810,7 +812,7 @@ async def search( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[SearchResponse]: """ Recall the most relevant learned context and source passages from a namespace. Hybrid search combines memories with document chunks by default, with optional query rewriting, reranking, and supporting context attachments. @@ -961,7 +963,7 @@ async def profile( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[ProfileResponse]: """ Read a continuously maintained understanding of the subject represented by this namespace. Stable facts, evolving context, and selected custom buckets are returned together without requiring a search query. @@ -1077,7 +1079,7 @@ async def list( extra_headers: typing.Optional[typing.Mapping[str, str]] = None, extra_query: typing.Optional[typing.Mapping[str, typing.Any]] = None, extra_body: typing.Optional[typing.Mapping[str, typing.Any]] = None, - timeout: typing.Union[float, httpx.Timeout, None] = None, + timeout: typing.Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN, ) -> AsyncHttpResponse[ListResponse]: """ Browse documents, source chunks, or learned memories through one predictable paginated contract. Choose the collection in the path; the other collection arrays remain empty for a stable response shape. diff --git a/tests/test_compat.py b/tests/test_compat.py index b537de67..d8cd3f62 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -144,6 +144,27 @@ def test_timeout_object_is_forwarded_whole() -> None: assert captured[1].extensions["timeout"]["read"] == 20.0 +def test_timeout_none_disables_timeouts() -> None: + from supermemory import NOT_GIVEN + + captured: list[httpx.Request] = [] + off = {"connect": None, "read": None, "write": None, "pool": None} + + _client(_ok(captured, [])).namespaces.list() + assert captured[0].extensions["timeout"] == {"connect": 5.0, "read": 60.0, "write": 60.0, "pool": 60.0} + + _client(_ok(captured, []), timeout=None).namespaces.list() + assert captured[1].extensions["timeout"] == off + + client = _client(_ok(captured, []), timeout=10.0) + client.namespaces.list(timeout=None) + assert captured[2].extensions["timeout"] == off + client.namespaces.list(timeout=NOT_GIVEN) + assert captured[3].extensions["timeout"]["read"] == 10.0 + client.with_options(timeout=None).namespaces.list() + assert captured[4].extensions["timeout"] == off + + def test_read_timeout_is_retried(monkeypatch: pytest.MonkeyPatch) -> None: import supermemory.core.http_client as http_client